diff --git a/backend/crates/gateway-core/src/engine/middleware/contract.rs b/backend/crates/gateway-core/src/engine/middleware/contract.rs index e6479db24..91f71c363 100644 --- a/backend/crates/gateway-core/src/engine/middleware/contract.rs +++ b/backend/crates/gateway-core/src/engine/middleware/contract.rs @@ -104,7 +104,6 @@ impl fmt::Debug for MiddlewareHeader { #[derive(Clone, PartialEq, Eq)] pub struct MiddlewareRequest { settings: Option, - provider_parameters: Option>, protocol: String, headers: Vec, body: Bytes, @@ -126,7 +125,6 @@ impl MiddlewareRequest { let protocol = protocol.into(); Self { settings: None, - provider_parameters: None, original_protocol: protocol.clone(), original_body: body.clone(), capabilities: None, @@ -147,27 +145,6 @@ impl MiddlewareRequest { self.settings.as_ref() } - /// Provider 提供的本次构造参数;Core 不解释其中的业务字段 - #[must_use] - pub fn provider_parameters(&self) -> Option<&serde_json::Map> { - self.provider_parameters.as_ref() - } - - #[must_use] - pub fn with_provider_parameters( - mut self, - parameters: serde_json::Map, - ) -> Self { - self.provider_parameters = Some(parameters); - self - } - - pub fn take_provider_parameters( - &mut self, - ) -> Option> { - self.provider_parameters.take() - } - #[must_use] pub fn protocol(&self) -> &str { &self.protocol diff --git a/backend/crates/gateway-core/src/engine/mod.rs b/backend/crates/gateway-core/src/engine/mod.rs index e713c9e3a..cabee6dc1 100644 --- a/backend/crates/gateway-core/src/engine/mod.rs +++ b/backend/crates/gateway-core/src/engine/mod.rs @@ -755,7 +755,6 @@ impl AttemptContext { provider: ProviderKind, model: Option, account_id: ProviderAccountId, - parameters: Option>, terminal: provider::ProviderMiddlewareTerminal, ) -> Result { // 入口已校验原请求,此处复查中间件最终提交的 operation,每次重试均生效 @@ -763,7 +762,7 @@ impl AttemptContext { .account_scope() .is_some_and(|scope| !scope.allows_image_generation()) { - Box::new(move |operation, headers, parameters| { + Box::new(move |operation, headers| { Box::pin(async move { image_generation::check_image_generation(false, &operation).map_err(|_| { ProviderError::new( @@ -772,7 +771,7 @@ impl AttemptContext { ) .with_retry_prohibited() })?; - terminal(operation, headers, parameters).await + terminal(operation, headers).await }) }) } else { @@ -790,7 +789,6 @@ impl AttemptContext { context, operation, self.request.client_transport, - parameters, terminal, ) .await diff --git a/backend/crates/gateway-core/src/engine/provider.rs b/backend/crates/gateway-core/src/engine/provider.rs index 1ae57ef58..90a8388a0 100644 --- a/backend/crates/gateway-core/src/engine/provider.rs +++ b/backend/crates/gateway-core/src/engine/provider.rs @@ -471,7 +471,6 @@ pub type ProviderMiddlewareTerminal = Box< dyn FnOnce( Operation, Vec, - Option>, ) -> BoxFuture<'static, Result> + Send + 'static, @@ -490,19 +489,18 @@ fn provider_middleware_next( activate: oneshot::Receiver<()>, observation: Arc>, ) -> MiddlewareNext { - crate::middleware::compose(Vec::new(), move |mut request| { + crate::middleware::compose(Vec::new(), move |request| { Box::pin(async move { let fallback_protocol = operation.protocol().to_owned(); if request.has_capability_declaration() { return Err(MiddlewareError::InvalidState); } - let parameters = request.take_provider_parameters(); let (protocol, headers, body) = request.into_parts(); validate_attempt_headers(&headers)?; let operation = operation .replace_middleware_wire(protocol, body) .map_err(|_| MiddlewareError::InvalidState)?; - let stream = terminal(operation, headers, parameters) + let stream = terminal(operation, headers) .await .map_err(MiddlewareError::Provider)?; let metadata = stream.metadata().clone(); @@ -736,22 +734,18 @@ pub async fn execute_attempt_middleware( context: MiddlewareContext, operation: Operation, transport: ClientTransport, - parameters: Option>, terminal: ProviderMiddlewareTerminal, ) -> Result { let Some(plan) = plan else { - return terminal(operation, Vec::new(), parameters).await; + return terminal(operation, Vec::new()).await; }; - let mut request = MiddlewareRequest::new( + let request = MiddlewareRequest::new( operation.protocol(), Vec::::new(), operation .middleware_body() .map_err(|_| middleware_protocol_error(UpstreamSendState::NotSent))?, ); - if let Some(parameters) = parameters { - request = request.with_provider_parameters(parameters); - } let (prepared, metadata) = oneshot::channel(); let (activate, activation) = oneshot::channel(); let observation = Arc::new(Mutex::new(MiddlewareSendObservation { diff --git a/backend/crates/gateway-core/tests/engine/execution/outbound.rs b/backend/crates/gateway-core/tests/engine/execution/outbound.rs index c4d5449cd..0747a684e 100644 --- a/backend/crates/gateway-core/tests/engine/execution/outbound.rs +++ b/backend/crates/gateway-core/tests/engine/execution/outbound.rs @@ -103,8 +103,7 @@ impl Provider for PreparingProvider { provider, model, account, - None, - Box::new(move |_, _, _| { + Box::new(move |_, _| { Box::pin(async move { let events = futures::stream::once(async move { Err::( diff --git a/backend/crates/gateway-core/tests/engine/middleware/cold.rs b/backend/crates/gateway-core/tests/engine/middleware/cold.rs index 3015d470d..93bb8087b 100644 --- a/backend/crates/gateway-core/tests/engine/middleware/cold.rs +++ b/backend/crates/gateway-core/tests/engine/middleware/cold.rs @@ -88,7 +88,7 @@ fn plan(action: Action) -> FrozenMiddlewarePlan { } fn terminal(polls: Arc, released: Arc) -> ProviderMiddlewareTerminal { - Box::new(move |_, _, _| { + Box::new(move |_, _| { Box::pin(async move { let metadata = ProviderCallMetadata::new( ProviderKind::new("openai").unwrap(), @@ -127,7 +127,6 @@ fn inspecting_a_response_waits_for_the_outer_stream_and_keeps_send_facts() { context(), operation(), ClientTransport::HttpJson, - None, terminal(polls.clone(), released.clone()), ) .await @@ -166,7 +165,6 @@ fn rejecting_metadata_drops_the_unactivated_handler_and_account_lease() { context(), operation(), ClientTransport::HttpJson, - None, terminal(polls.clone(), released.clone()), ) .await @@ -196,8 +194,7 @@ fn middleware_rejection_before_next_does_not_wait_for_activation() { context(), operation(), ClientTransport::HttpJson, - None, - Box::new(|_, _, _| panic!("rejected middleware cannot prepare upstream")), + Box::new(|_, _| panic!("rejected middleware cannot prepare upstream")), ) .await .err() diff --git a/backend/crates/gateway-core/tests/engine/middleware/mod.rs b/backend/crates/gateway-core/tests/engine/middleware/mod.rs index b86b834fb..492b6c370 100644 --- a/backend/crates/gateway-core/tests/engine/middleware/mod.rs +++ b/backend/crates/gateway-core/tests/engine/middleware/mod.rs @@ -340,7 +340,7 @@ fn attempt_middleware_consumes_owned_next_once_and_preserves_host_envelopes() { let terminal: ProviderMiddlewareTerminal = { let terminal_calls = Arc::clone(&terminal_calls); - Box::new(move |operation, headers, _| { + Box::new(move |operation, headers| { terminal_calls.fetch_add(1, Ordering::SeqCst); Box::pin(async move { assert!(headers.is_empty()); @@ -392,7 +392,6 @@ fn attempt_middleware_consumes_owned_next_once_and_preserves_host_envelopes() { context(), operation(), ClientTransport::HttpJson, - None, terminal, ) .await @@ -487,7 +486,7 @@ fn attempt_passthrough_preserves_parsed_wire_for_every_client_transport() { payload.clone(), ) .unwrap(); - let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _, _| { + let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _| { Box::pin(async move { Ok(ProviderStream::new( empty_provider_stream().metadata().clone(), @@ -501,7 +500,6 @@ fn attempt_passthrough_preserves_parsed_wire_for_every_client_transport() { context(), operation(), transport, - None, terminal, ) .await @@ -569,7 +567,7 @@ fn attempt_sse_encoding_keeps_opaque_metadata_from_creating_extra_frames() { Some(10), ) .unwrap(); - let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _, _| { + let terminal: ProviderMiddlewareTerminal = Box::new(move |_, _| { Box::pin(async move { Ok(ProviderStream::new( empty_provider_stream().metadata().clone(), @@ -584,7 +582,6 @@ fn attempt_sse_encoding_keeps_opaque_metadata_from_creating_extra_frames() { context(), operation(), ClientTransport::HttpSse, - None, terminal, ) .await @@ -599,7 +596,7 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() { let allowed_calls = Arc::new(AtomicUsize::new(0)); let allowed_terminal: ProviderMiddlewareTerminal = { let allowed_calls = Arc::clone(&allowed_calls); - Box::new(move |_operation, headers, _| { + Box::new(move |_operation, headers| { allowed_calls.fetch_add(1, Ordering::SeqCst); Box::pin(async move { assert_eq!(headers.len(), 2); @@ -627,7 +624,6 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() { context(), operation(), ClientTransport::HttpJson, - None, allowed_terminal, ) .await @@ -637,7 +633,7 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() { let calls = Arc::new(AtomicUsize::new(0)); let terminal: ProviderMiddlewareTerminal = { let calls = calls.clone(); - Box::new(move |_, headers, _| { + Box::new(move |_, headers| { calls.fetch_add(1, Ordering::SeqCst); Box::pin(async move { assert_eq!(headers[0].name(), "authorization"); @@ -661,7 +657,6 @@ fn attempt_middleware_preserves_multi_value_and_authentication_headers() { context(), operation(), ClientTransport::HttpJson, - None, terminal, ) .await @@ -741,8 +736,7 @@ fn attempt_middleware_cannot_add_image_generation_to_a_denied_scope() { ProviderKind::new("openai").unwrap(), Some("gpt-test".into()), ProviderAccountId::new("acct_middleware").unwrap(), - None, - Box::new(move |operation, _, _| { + Box::new(move |operation, _| { Box::pin(async move { assert!(operation.image_generation_requested()); count.fetch_add(1, Ordering::SeqCst); diff --git a/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs b/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs index 58af1b191..82cc532ad 100644 --- a/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs +++ b/backend/crates/gateway-plugin/runtime/src/adapter/middleware/request.rs @@ -98,7 +98,6 @@ pub(super) async fn invoke( scope, )?; let head = MiddlewareRequestHead { - provider_parameters: invocation.provider_parameters(), settings_sources: invocation.settings_sources(), settings: invocation .settings() diff --git a/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs b/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs index a2b37e43b..d3792438d 100644 --- a/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs +++ b/backend/crates/gateway-plugin/runtime/src/callback/middleware/mod.rs @@ -135,15 +135,6 @@ pub(crate) enum MiddlewareCompletionBody { } impl MiddlewareInvocation { - pub(crate) fn provider_parameters(&self) -> Option> { - self.state - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .original_request - .provider_parameters() - .cloned() - } - pub(crate) fn settings(&self) -> Result { let state = self .state @@ -343,12 +334,6 @@ impl MiddlewareInvocation { request: MiddlewareNextRequest, payload: Vec, ) -> Result { - if let Some(parameters) = request.provider_parameters { - if original.provider_parameters().is_none() { - return Err(invalid()); - } - original = original.with_provider_parameters(parameters); - } if let Some(settings) = request.settings { let current = original.settings().ok_or_else(|| { PluginFault::new( diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs b/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs index 3db3774c1..5c9366196 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/policy/facts/mod.rs @@ -71,8 +71,7 @@ async fn reading_websocket_facts_preserves_original_text_and_opaque_messages() { attempt_middleware_context_for_transport(Arc::default(), ClientTransport::WebSocket), operation(), ClientTransport::WebSocket, - None, - Box::new(move |_, _, _| { + Box::new(move |_, _| { Box::pin(async move { Ok(ProviderStream::new( ProviderCallMetadata::new( @@ -228,8 +227,7 @@ async fn facts_are_readable_without_replacing_or_duplicating_the_original_execut attempt_middleware_context(Arc::default()), operation(), ClientTransport::HttpJson, - None, - Box::new(move |_, _, _| { + Box::new(move |_, _| { Box::pin(async move { Ok(ProviderStream::new( metadata, diff --git a/backend/crates/gateway-plugin/runtime/tests/generation/policy/lifecycle.rs b/backend/crates/gateway-plugin/runtime/tests/generation/policy/lifecycle.rs index 37f8e3a31..c8f49d63f 100644 --- a/backend/crates/gateway-plugin/runtime/tests/generation/policy/lifecycle.rs +++ b/backend/crates/gateway-plugin/runtime/tests/generation/policy/lifecycle.rs @@ -3,72 +3,6 @@ use super::*; use gateway_admin::ports::plugins::PluginPreparation; -#[tokio::test] -async fn provider_parameters_compose_through_sdk_without_merging_deleted_fields() { - let worker = std::fs::read(env!("CARGO_BIN_EXE_gateway-plugin-test-middleware")).unwrap(); - let package = crate::support::package_with_contributions( - &worker, - Contributions::from([crate::support::contribution( - Capability::Middleware, - vec![Stage::Attempt], - vec!["openai".into()], - vec!["openai".into()], - )]), - ); - let original = serde_json::json!({"installation_id":"account-default", "future":{"keep":true}}); - let first = serde_json::json!({"installation_id":"plugin-first"}); - let last = serde_json::json!({}); - let instances = [ - ("parameters-first", original.clone(), first.clone()), - ("parameters-last", first, serde_json::Value::Null), - ] - .into_iter() - .enumerate() - .map(|(index, (id, expected, parameters))| InstanceFixture { - id, - configuration: serde_json::json!({"mode":"parameters", "attempt":true, - "expected_parameters":expected, "parameters":parameters}), - bindings: vec![binding( - MIDDLEWARE_CONTRIBUTION, - "attempt", - index as i32, - PluginFailurePolicy::Reject, - )], - }) - .collect(); - let (cache, runtime) = setup_package(instances, package).await; - let generation = prepare(&runtime).await; - let plan = runtime - .execution_registry() - .middleware(&generation) - .unwrap(); - let downstream = Downstream { - calls: Arc::default(), - closes: Arc::default(), - reads: Arc::default(), - error: false, - }; - let response = plan - .handle( - attempt_middleware_context(Arc::default()), - budget_request().with_provider_parameters(original.as_object().unwrap().clone()), - gateway_core::middleware::compose(Vec::new(), move |request| { - assert_eq!( - request.provider_parameters().unwrap(), - last.as_object().unwrap() - ); - downstream.into_next().run(request) - }), - ) - .await - .unwrap(); - let (_, _, _, body, _) = response.into_parts(); - body.close().await; - drop(plan); - drop(generation); - super::super::wait_until_empty(cache.path()).await; -} - #[tokio::test] async fn partial_update_preserves_inflight_middleware_and_uses_new_configuration_for_new_calls() { let (cache, store, runtime) = super::super::setup_with_contributions(Contributions::from([ diff --git a/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs b/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs index 8d3f74dee..e035a7896 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/middleware/mod.rs @@ -545,19 +545,6 @@ async fn main() -> Result<(), Box> { call.request.head.settings = settings.clone(); } } - if mode == "parameters" { - assert_eq!( - serde_json::to_value(&call.request.head.provider_parameters).unwrap(), - configuration["expected_parameters"], - ); - if let Some(parameters) = configuration.get("parameters") { - call.request.head.provider_parameters = if parameters.is_null() { - None - } else { - Some(parameters.as_object().unwrap().clone()) - }; - } - } if mode == "inspect" && !call.request.body.is_empty() { // 读取和解析只能作用于副本,不能把解析结果写回未改写的正文 assert!(serde_json::from_slice::(&call.request.body).is_ok()); diff --git a/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs b/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs index bad73a66f..a1607c334 100644 --- a/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs +++ b/backend/crates/gateway-plugin/runtime/tests/support/worker/mod.rs @@ -1046,7 +1046,6 @@ impl Peer { id, NEXT_METHOD, serde_json::to_value(MiddlewareNextRequest { - provider_parameters: None, settings: None, protocol: None, header_mutations: Vec::new(), diff --git a/backend/crates/gateway-plugin/sdk/docs/capabilities.md b/backend/crates/gateway-plugin/sdk/docs/capabilities.md index 461517aea..0a518c9bc 100644 --- a/backend/crates/gateway-plugin/sdk/docs/capabilities.md +++ b/backend/crates/gateway-plugin/sdk/docs/capabilities.md @@ -402,7 +402,12 @@ Ping 的自动 Pong 与关闭握手属于底层 WebSocket 协议事实;插件 所有外层处理结束后,Provider 才把结果交给 transport 发送。插件错误、非法传输值或正文超限在原生发送前结束, 不自动跳过、重放或切换传输;取消、请求截止与回调资源沿用当前 attempt +身份策略可在 `request` / `attempt` 读取原始输入,以真实 Key 与账号标识调用私有状态服务维护映射, +再在 `outbound` 自行改写完整请求中的身份及关联字段。UUID 生成、并发创建、持久化、会话与缓存标识派生均由插件负责; +通用中间件层不解释 installation ID、device ID 等策略字段,也不在插件输出后重新派生业务字段 + WebSocket 的 `body` 是准备发送的一条完整 UTF-8 文本消息,`headers` 和 `uri` 是候选 opening。 +新响应链按出站改写产生的 URI / 握手头差异分区,精确续接仍使用持有原响应链的连接。 复用连接时不能改写已发生的握手;宿主不为插件主动重连、重放消息或重建续接。 插件需要握手身份与消息内容一致时,应结合实际连接与续接语义选择改写 @@ -429,17 +434,6 @@ request 与 attempt 阶段通过 `append_header` 添加的请求头会进入上 OpenAI 与 xAI 的上游 headers 先由账号和画像生成,插件同名字段覆盖基线,同名多值按插件顺序保留;不存在按认证、Cookie 或会话名称划分的保护列表。 实际账号租约、凭据版本与计费归属仍记录真实选中事实,不随插件修改 header 名称而伪造 -attempt 的 `request.head.provider_parameters` 携带 Provider 提供的本次构造参数;没有该能力的执行不提供此对象。 -OpenAI 原生 Responses、Images、Search 与 Live 创建请求提供 `installation_id` 字符串,默认取自选中账号, -用于原生安装身份、已有 turn metadata 身份字段及会话、缓存关联标识的派生。独立 `upstream_adapter` 自行构造其请求。 - -插件直接修改此对象,SDK 在值改变时提交完整替换;空对象明确删除全部参数,将原本存在的对象设为 `None` 同样提交空对象。 -线协议的 `next.provider_parameters` 省略时保留原值,显式 `null` 或非对象类型拒绝。 -后续插件读取前层结果,Core / Runtime 不解释业务字段、不深合并;缺失必需字段或类型错误在发送前失败且禁止重试,不补回账号默认身份。 - -身份选择、Key 与账号配对、UUID 生成与持久化由插件负责,可复用已有状态服务;参数不写回账号凭据、全局设置或真实计费事实。 -WS 新响应链按构造身份区分连接,精确续接仍使用实际持有该响应链的原连接;Live sideband / hangup 不继承创建请求的临时参数 - request 阶段 `request.head.settings` 是本次调用的有效设置对象,包含 `runtime`、`fast_mode`、`client_limits` 和 `timeout_ms`。 `fast_mode` 使用 `default`、`enabled`、`disabled` 三态,具体档位语义见[账号分组](../../../../../docs/api.md#6-账号分组)。 `runtime` 使用宿主快照的设置字段,画像已合并 Client Key 设置;可以修改映射、画像、调度、位置及计价等执行参数。 diff --git a/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs b/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs index 35ed53ff5..21c071385 100644 --- a/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/call/middleware/mod.rs @@ -70,9 +70,6 @@ pub struct MiddlewareRequestHead { /// 当前请求的完整有效执行设置;尚未建立可改写设置的边界使用 null #[serde(default)] pub settings: serde_json::Value, - /// 本次 Provider 构造参数;仅原生 attempt 提供 - #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider_parameters: Option>, pub request_id: String, pub client_key_id: String, pub account_group_ids: Vec, @@ -131,13 +128,6 @@ pub struct MiddlewareNextRequest { deserialize_with = "deserialize_replacement" )] pub settings: Option, - /// 完整替换构造参数;空对象仍表示显式替换,不隐式合并或补回删除字段 - #[serde( - default, - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_replacement" - )] - pub provider_parameters: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub protocol: Option, #[serde(default)] diff --git a/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs b/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs index 4c340642a..79bd6ba8d 100644 --- a/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs +++ b/backend/crates/gateway-plugin/sdk/src/client/middleware/mod.rs @@ -39,7 +39,6 @@ pub struct MiddlewareRequest { pub body: Vec, original_protocol: String, original_settings: serde_json::Value, - original_provider_parameters: Option>, original_headers: Vec, original_body: Vec, body_replaced: bool, @@ -99,9 +98,6 @@ impl MiddlewareRequest { }; ( MiddlewareNextRequest { - provider_parameters: (self.head.provider_parameters - != self.original_provider_parameters) - .then(|| self.head.provider_parameters.unwrap_or_default()), settings: (self.head.settings != self.original_settings) .then_some(self.head.settings), protocol, @@ -345,7 +341,6 @@ impl RequestCall { context: call.context, request: MiddlewareRequest { original_settings: head.settings.clone(), - original_provider_parameters: head.provider_parameters.clone(), body: call.payload, original_body, body_replaced: false, diff --git a/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs b/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs index 8401274c0..65a1c73a6 100644 --- a/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs +++ b/backend/crates/gateway-plugin/sdk/tests/call/middleware/mod.rs @@ -13,37 +13,6 @@ use gateway_plugin_sdk::{ }; use serde_json::json; -#[test] -fn provider_parameters_distinguish_preserve_empty_and_invalid_replacement() { - let preserved: MiddlewareNextRequest = - serde_json::from_value(json!({"body":"preserve"})).unwrap(); - assert!(preserved.provider_parameters.is_none()); - for replacement in [json!({}), json!({"installation_id":null})] { - let request: MiddlewareNextRequest = serde_json::from_value(json!({ - "body":"preserve", "provider_parameters":replacement, - })) - .unwrap(); - assert_eq!( - request.provider_parameters.as_ref(), - replacement.as_object() - ); - } - for invalid in [ - json!(null), - json!([]), - json!("invalid"), - json!(42), - json!(false), - ] { - assert!( - serde_json::from_value::(json!({ - "body":"preserve", "provider_parameters":invalid, - })) - .is_err() - ); - } -} - #[test] fn settings_replacement_preserves_explicit_null_for_host_validation() { let preserved: MiddlewareNextRequest = @@ -75,7 +44,6 @@ fn middleware_capability_and_mounts_have_one_stable_wire_vocabulary() { #[test] fn middleware_request_keeps_body_out_of_json_and_rejects_unknown_fields() { let request = MiddlewareRequestHead { - provider_parameters: None, settings_sources: serde_json::Value::Null, settings: serde_json::Value::Null, client_key_id: "fixture-key".into(), @@ -113,7 +81,6 @@ fn middleware_request_keeps_body_out_of_json_and_rejects_unknown_fields() { #[test] fn next_distinguishes_preserve_from_replace_empty_and_uses_incremental_headers() { let preserve = MiddlewareNextRequest { - provider_parameters: None, settings: None, protocol: None, header_mutations: Vec::new(), @@ -126,7 +93,6 @@ fn next_distinguishes_preserve_from_replace_empty_and_uses_incremental_headers() ); let replace = MiddlewareNextRequest { - provider_parameters: None, settings: None, protocol: Some("xai".into()), header_mutations: vec![ diff --git a/backend/crates/gateway-plugin/sdk/tests/client/session/mod.rs b/backend/crates/gateway-plugin/sdk/tests/client/session/mod.rs index 0ebef12d4..f0292a39f 100644 --- a/backend/crates/gateway-plugin/sdk/tests/client/session/mod.rs +++ b/backend/crates/gateway-plugin/sdk/tests/client/session/mod.rs @@ -391,7 +391,6 @@ fn middleware_context(id: u64) -> CallContext { pub(super) fn middleware_request() -> MiddlewareRequestHead { MiddlewareRequestHead { - provider_parameters: None, settings_sources: serde_json::Value::Null, settings: serde_json::Value::Null, client_key_id: "fixture-key".into(), diff --git a/backend/crates/providers/openai/src/provider/execution.rs b/backend/crates/providers/openai/src/provider/execution.rs index a87bb15ba..07e21dc3b 100644 --- a/backend/crates/providers/openai/src/provider/execution.rs +++ b/backend/crates/providers/openai/src/provider/execution.rs @@ -179,8 +179,7 @@ impl CodexProvider { provider_kind, requested_model.map(|model| model.as_str().to_owned()), account_id, - Some(RequestParameters::baseline(lease.installation_id())), - Box::new(move |operation, middleware_headers, parameters| { + Box::new(move |operation, middleware_headers| { Box::pin(async move { provider .execute_selected_raw_json_endpoint( @@ -188,11 +187,8 @@ impl CodexProvider { operation, middleware_headers, request, - SelectedRequest { - parameters: RequestParameters::decode(parameters)?, - lease, - account_selection_wait_ms, - }, + lease, + account_selection_wait_ms, ) .await }) @@ -207,13 +203,9 @@ impl CodexProvider { operation: Operation, middleware_headers: Vec, mut request: RawJsonEndpointRequest, - selected: SelectedRequest, + mut lease: CodexCredentialLease, + account_selection_wait_ms: u64, ) -> Result { - let SelectedRequest { - parameters, - mut lease, - account_selection_wait_ms, - } = selected; let affinity = match &operation { Operation::GenerateImage(image) => { let model = endpoint_requested_model(image.payload()); @@ -337,7 +329,7 @@ impl CodexProvider { lease.capacity_snapshot(), )); // Standalone Provider 端点没有可证明的账号 owner;Images/Search metadata 必须按 - // 跨账号输入收敛到本次构造参数,不能沿用下游声明的账号身份 + // 跨账号输入收敛到当前 lease,不能沿用下游声明的账号或 installation identity let turn_metadata = payload .context() .get("turn_metadata") @@ -345,12 +337,11 @@ impl CodexProvider { .and_then(|metadata| { crate::transport::request::scope_turn_metadata( metadata, - ¶meters.installation_id, + lease.installation_id(), true, ) }); let events = cold_json_response_stream(ColdJsonResponse { - parameters, client: self .client_for_request(&context)? .for_account(lease.account()) @@ -392,7 +383,6 @@ struct RawJsonEndpointRequest { } pub(super) struct ColdResponse { - pub(super) parameters: RequestParameters, pub(super) client: CodexBackendClient, pub(super) response_origin: Url, pub(super) request: CodexResponsesRequest, @@ -412,7 +402,6 @@ pub(super) struct ColdResponse { } pub(super) struct ColdJsonResponse { - pub(super) parameters: RequestParameters, pub(super) client: CodexBackendClient, pub(super) response_origin: Url, pub(super) endpoint_path: &'static str, @@ -657,7 +646,7 @@ pub(super) fn cold_json_response_stream(request: ColdJsonResponse) -> EventStrea let response = create_json_attempt( &request, &active_account, - &request.parameters.installation_id, + request.lease.installation_id(), &authorization, cookie_header.as_ref(), account_selection, @@ -776,7 +765,6 @@ fn image_response_metering( pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { let ColdResponse { - parameters, client, response_origin, request, @@ -833,7 +821,7 @@ pub(super) fn cold_response_stream(response: ColdResponse) -> EventStream { &request, &request_id, &active_account, - ¶meters.installation_id, + lease.installation_id(), &authorization, cookie_header.as_ref(), account_selection, diff --git a/backend/crates/providers/openai/src/provider/live.rs b/backend/crates/providers/openai/src/provider/live.rs index 8a13454ee..94a032e6d 100644 --- a/backend/crates/providers/openai/src/provider/live.rs +++ b/backend/crates/providers/openai/src/provider/live.rs @@ -280,8 +280,7 @@ impl CodexProvider { provider_kind, Some(upstream_model.as_str().to_owned()), account_id, - Some(RequestParameters::baseline(lease.installation_id())), - Box::new(move |operation, middleware_headers, parameters| { + Box::new(move |operation, middleware_headers| { Box::pin(async move { provider .execute_selected_live_call( @@ -289,11 +288,8 @@ impl CodexProvider { operation, upstream_model, middleware_headers, - SelectedRequest { - parameters: RequestParameters::decode(parameters)?, - lease, - account_selection_wait_ms, - }, + lease, + account_selection_wait_ms, ) .await }) @@ -308,13 +304,9 @@ impl CodexProvider { operation: Operation, upstream_model: UpstreamModelId, middleware_headers: Vec, - selected: SelectedRequest, + mut lease: CodexCredentialLease, + account_selection_wait_ms: u64, ) -> Result { - let SelectedRequest { - parameters, - mut lease, - account_selection_wait_ms, - } = selected; let Operation::ProviderHttp(request) = operation else { return Err(provider_error( ProviderErrorKind::InvalidRequest, @@ -374,7 +366,6 @@ impl CodexProvider { .cloned() .collect::>(); let events = cold_live_call_stream(ColdLiveCall { - parameters, client: self .client_for_request(&context)? .for_account(lease.account()) @@ -407,7 +398,6 @@ impl CodexProvider { } struct ColdLiveCall { - parameters: RequestParameters, client: CodexBackendClient, registry: Arc, response_origin: Url, @@ -451,7 +441,7 @@ fn cold_live_call_stream(request: ColdLiveCall) -> EventStream { authorization.expose_secret(), active_account.upstream_account_id(), &request_id, - Some(&request.parameters.installation_id), + Some(request.lease.installation_id()), ); request_context.trace = Some(&trace); request_context.account_selection = CodexAccountSelectionTelemetry::new( diff --git a/backend/crates/providers/openai/src/provider/mod.rs b/backend/crates/providers/openai/src/provider/mod.rs index 03fb72e27..5ca2730a0 100644 --- a/backend/crates/providers/openai/src/provider/mod.rs +++ b/backend/crates/providers/openai/src/provider/mod.rs @@ -107,7 +107,6 @@ mod execution; mod failure; mod live; mod observation; -mod parameters; mod upstream_adapter; mod workers; pub(crate) use live::{CodexLiveGateway, CodexLiveRegistry}; @@ -118,7 +117,6 @@ use execution::*; pub use failure::openai_failure_affects_account_score; use failure::*; use observation::*; -use parameters::RequestParameters; pub(crate) use workers::worker_contributions; const PROVIDER_NAME: &str = "openai"; @@ -221,14 +219,9 @@ impl PreparedGenerateRequest { } struct SelectedGenerate { - request: SelectedRequest, - frozen_requirements: CapabilityRequirements, -} - -struct SelectedRequest { lease: CodexCredentialLease, account_selection_wait_ms: u64, - parameters: RequestParameters, + frozen_requirements: CapabilityRequirements, } impl CodexProvider { @@ -756,10 +749,7 @@ impl Provider for CodexProvider { provider_kind, Some(upstream_model.as_str().to_owned()), account_id, - adapter - .is_none() - .then(|| RequestParameters::baseline(lease.installation_id())), - Box::new(move |operation, middleware_headers, parameters| { + Box::new(move |operation, middleware_headers| { Box::pin(async move { if let Some(adapter) = adapter { return provider.execute_upstream_adapter( @@ -780,11 +770,8 @@ impl Provider for CodexProvider { terminal_model, terminal_context, SelectedGenerate { - request: SelectedRequest { - parameters: RequestParameters::decode(parameters)?, - lease, - account_selection_wait_ms, - }, + lease, + account_selection_wait_ms, frozen_requirements, }, ) @@ -806,12 +793,8 @@ impl CodexProvider { selected: SelectedGenerate, ) -> Result { let SelectedGenerate { - request: - SelectedRequest { - parameters, - mut lease, - account_selection_wait_ms, - }, + mut lease, + account_selection_wait_ms, frozen_requirements, } = selected; let Operation::Generate(generate) = operation else { @@ -951,7 +934,7 @@ impl CodexProvider { .then(|| crate::transport::diagnostics::evidence::request_state(&upstream_request)); scope_request_to_account( &mut upstream_request, - ¶meters.installation_id, + lease.installation_id(), account_scope, ); if let Some(before) = state_before { @@ -1106,7 +1089,6 @@ impl CodexProvider { AttemptTransport::Default | AttemptTransport::Fallback => 0, }; let events = cold_response_stream(ColdResponse { - parameters, client: self .client_for_request(&context)? .for_account(lease.account()) diff --git a/backend/crates/providers/openai/src/provider/parameters.rs b/backend/crates/providers/openai/src/provider/parameters.rs deleted file mode 100644 index ac0e3d95a..000000000 --- a/backend/crates/providers/openai/src/provider/parameters.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! OpenAI 原生请求的构造参数;只解释本次值,不维护插件身份策略 - -use gateway_core::{ - error::{ProviderError, ProviderErrorKind}, - upstream::UpstreamSendState, -}; -use serde::Deserialize; -use serde_json::{Map, Value}; - -#[derive(Deserialize)] -pub(super) struct RequestParameters { - pub(super) installation_id: String, -} - -impl RequestParameters { - pub(super) fn baseline(installation_id: &str) -> Map { - Map::from_iter([( - "installation_id".into(), - Value::String(installation_id.to_owned()), - )]) - } - - pub(super) fn decode(parameters: Option>) -> Result { - let invalid = || { - ProviderError::new( - ProviderErrorKind::InvalidRequest, - UpstreamSendState::NotSent, - ) - .with_retry_prohibited() - }; - // 参数基线已在选号后提供,显式删除和类型错误不能回退为账号默认身份 - serde_json::from_value(Value::Object(parameters.ok_or_else(invalid)?)) - .map_err(|_| invalid()) - } -} diff --git a/backend/crates/providers/openai/src/transport/client_sse.rs b/backend/crates/providers/openai/src/transport/client_sse.rs index e1f778ff8..24f437014 100644 --- a/backend/crates/providers/openai/src/transport/client_sse.rs +++ b/backend/crates/providers/openai/src/transport/client_sse.rs @@ -622,10 +622,7 @@ impl CodexBackendClient { let mut key = CodexWebSocketPoolKey::new(&self.base_url, account_id, conversation_id) .with_egress_key(&self.egress_key) .with_outbound_proxy(self.outbound_proxy.as_ref()) - .with_connection_profile(format!( - "{connection_profile}\0{}", - context.installation_id.unwrap_or_default(), - )); + .with_connection_profile(connection_profile); if let Some(connection_id) = request.downstream_websocket_connection_id.as_deref() { key = key.with_downstream_connection_id(connection_id); } diff --git a/backend/crates/providers/openai/tests/provider/contract/correlation.rs b/backend/crates/providers/openai/tests/provider/contract/correlation.rs index f3c0d1986..58889c1e5 100644 --- a/backend/crates/providers/openai/tests/provider/contract/correlation.rs +++ b/backend/crates/providers/openai/tests/provider/contract/correlation.rs @@ -2,337 +2,6 @@ use super::*; -#[derive(Debug)] -struct ParametersMiddleware(Map); - -impl MiddlewarePlan for ParametersMiddleware { - fn handle( - &self, - context: MiddlewareContext, - request: MiddlewareRequest, - next: MiddlewareNext, - ) -> BoxFuture<'static, Result> { - assert_eq!(context.mount(), MiddlewareMount::Attempt); - assert!(request.provider_parameters().unwrap()["installation_id"].is_string()); - next.run(request.with_provider_parameters(self.0.clone())) - } -} - -#[tokio::test] -async fn request_parameters_drive_native_identity_and_derivation_across_http_and_websocket() { - let store = Arc::new(MemoryAccountStore::default()); - create_account(&store, "acct_provider_contract").await; - let identities = [ - "f77b9b8e-563a-4d84-a66f-f2a0c98d5a8e", - "f6e974b5-708d-4940-b619-1ab2c58c2f2c", - ]; - let mut captures = Vec::new(); - for identity in identities { - for websocket in [false, true] { - let (base_url, capture): ( - String, - BoxFuture<'static, (reqwest::header::HeaderMap, Value)>, - ) = if websocket { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let url = format!("http://{}", listener.local_addr().unwrap()); - let task = tokio::spawn(async move { - let (socket, _) = listener.accept().await.unwrap(); - let mut headers = None; - let mut socket = - crate::transport::accept_codex_test_websocket_with(socket, |request, _| { - headers = Some(request.headers().clone()) - }) - .await; - let frame = socket.next().await.unwrap().unwrap(); - let body = serde_json::from_str(frame.to_text().unwrap()).unwrap(); - socket.send(Message::Text(json!({"type":"response.completed", "response":{ - "id":"resp_parameters", "model":"gpt-5.4", "status":"completed", "output":[] - }}).to_string().into())).await.unwrap(); - (headers.unwrap(), body) - }); - (url, Box::pin(async { task.await.unwrap() })) - } else { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/codex/responses")) - .respond_with( - ResponseTemplate::new(200) - .insert_header("content-type", "text/event-stream") - .set_body_string(CAPTURE_COMPLETED_SSE), - ) - .expect(1) - .mount(&server) - .await; - ( - server.uri(), - Box::pin(async move { - let requests = server.received_requests().await.unwrap(); - ( - requests[0].headers.clone(), - captured_request_body(&requests[0]), - ) - }), - ) - }; - let payload = payload(websocket); - let mut request_context = payload.context().clone(); - let mut turn_metadata = metadata(); - turn_metadata["installation_id"] = json!("client-installation"); - request_context.insert("turn_metadata".into(), json!(turn_metadata.to_string())); - let mut stream = provider_with_base_url(&store, base_url) - .execute( - planned_request( - "openai", - Operation::Generate(GenerateRequest::from_protocol_payload( - payload.with_context(request_context), - )), - ), - context_with_middleware( - "req_parameters", - Arc::new(ParametersMiddleware(Map::from_iter([( - "installation_id".into(), - json!(identity), - )]))), - FastMode::Default, - ), - ) - .await - .unwrap(); - assert_eq!( - stream.metadata().provider_account_id().as_str(), - "acct_provider_contract" - ); - while let Some(event) = stream.next().await { - event.unwrap(); - } - let (headers, body) = capture.await; - assert_eq!(body["client_metadata"]["x-codex-installation-id"], identity); - let metadata: Value = - serde_json::from_str(headers["x-codex-turn-metadata"].to_str().unwrap()).unwrap(); - assert_eq!(metadata["installation_id"], identity); - assert_eq!( - headers["session-id"].to_str().unwrap(), - body["client_metadata"]["session_id"] - ); - captures.push(( - body["client_metadata"]["session_id"].clone(), - body["prompt_cache_key"].clone(), - )); - } - } - assert_eq!(captures[0], captures[1]); - assert_eq!(captures[2], captures[3]); - assert_ne!(captures[0].0, captures[2].0); - assert_ne!(captures[0].1, captures[2].1); -} - -#[tokio::test] -async fn request_parameters_partition_new_websocket_chains_without_preventing_reuse() { - let store = Arc::new(MemoryAccountStore::default()); - create_account(&store, "acct_provider_contract").await; - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let url = format!("http://{}", listener.local_addr().unwrap()); - let identities = ["parameters-first", "parameters-second"]; - let server = tokio::spawn(async move { - let mut retained = Vec::new(); - for (index, identity) in identities.into_iter().enumerate() { - let (socket, _) = tokio::time::timeout(Duration::from_secs(3), listener.accept()) - .await - .unwrap() - .unwrap(); - let mut socket = - crate::transport::accept_codex_test_websocket_with(socket, |request, _| { - let metadata: Value = serde_json::from_str( - request.headers()["x-codex-turn-metadata"].to_str().unwrap(), - ) - .unwrap(); - assert_eq!(metadata["installation_id"], identity); - }) - .await; - for turn in 0..=index { - let text = tokio::time::timeout(Duration::from_secs(3), async { - loop { - match socket.next().await.unwrap().unwrap() { - Message::Text(text) => break text, - Message::Ping(payload) => { - socket.send(Message::Pong(payload)).await.unwrap() - } - message => panic!("unexpected websocket frame: {message:?}"), - } - } - }) - .await - .unwrap(); - let body: Value = serde_json::from_str(&text).unwrap(); - assert_eq!(body["client_metadata"]["x-codex-installation-id"], identity); - socket.send(Message::Text(json!({"type":"response.completed", "response":{ - "id":format!("resp_parameters_{index}_{turn}"), "model":"gpt-5.4", "status":"completed", "output":[] - }}).to_string().into())).await.unwrap(); - } - // 保持旧连接可复用,证明新身份的隔离不是由测试服务端断开触发 - retained.push(socket); - } - }); - let provider = provider_with_base_url(&store, url); - for identity in [identities[0], identities[1], identities[1]] { - let payload = payload(true); - let mut request_context = payload.context().clone(); - request_context.insert( - "turn_metadata".into(), - json!(json!({"installation_id":"client-installation"}).to_string()), - ); - let mut stream = Arc::clone(&provider) - .execute( - planned_request( - "openai", - Operation::Generate( - GenerateRequest::from_protocol_payload( - payload.with_context(request_context), - ) - .with_provider_session_state( - ProviderSessionState::new( - "openai", - Map::from_iter([ - ("account_id".into(), json!("acct_provider_contract")), - ("conversation_id".into(), json!("parameters-conversation")), - ("continuation_scope".into(), json!("persisted")), - ]), - ) - .unwrap(), - ), - ), - ), - context_with_middleware( - "req_parameters_pool", - Arc::new(ParametersMiddleware(Map::from_iter([( - "installation_id".into(), - json!(identity), - )]))), - FastMode::Default, - ), - ) - .await - .unwrap(); - tokio::time::timeout(Duration::from_secs(5), async { - while let Some(event) = stream.next().await { - event.unwrap(); - } - }) - .await - .unwrap(); - } - server.await.unwrap(); -} - -#[tokio::test] -async fn request_parameters_apply_to_images_and_search_without_reencoding_opaque_bodies() { - let store = Arc::new(MemoryAccountStore::default()); - create_account(&store, "acct_provider_contract").await; - let server = MockServer::start().await; - Mock::given(method("POST")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({"data":[]}))) - .expect(3) - .mount(&server) - .await; - let provider = provider_with_base_url(&store, server.uri()); - let body = Bytes::from_static(br#"{ "future":1, "future":2 }"#); - let raw = RawJsonPayload::new("openai", body.clone()) - .unwrap() - .with_context(Map::from_iter([( - "turn_metadata".into(), - json!(json!({"installation_id":"client-installation"}).to_string()), - )])); - for operation in [ - Operation::GenerateImage(ImageRequest::from_raw_json( - ImageRequestKind::Generation, - raw.clone(), - )), - Operation::GenerateImage(ImageRequest::from_raw_json( - ImageRequestKind::Edit, - raw.clone(), - )), - Operation::Search(StandaloneSearchRequest::from_raw_json(raw)), - ] { - let mut stream = Arc::clone(&provider) - .execute( - planned_provider_endpoint_request("openai", operation), - context_with_middleware( - "req_parameters_json", - Arc::new(ParametersMiddleware(Map::from_iter([( - "installation_id".into(), - json!("plugin-installation"), - )]))), - FastMode::Default, - ), - ) - .await - .unwrap(); - while let Some(event) = stream.next().await { - event.unwrap(); - } - } - for request in server.received_requests().await.unwrap() { - assert_eq!(request.body.as_slice(), body.as_ref()); - let metadata: Value = - serde_json::from_str(request.headers["x-codex-turn-metadata"].to_str().unwrap()) - .unwrap(); - assert_eq!(metadata["installation_id"], "plugin-installation"); - } -} - -#[tokio::test] -async fn invalid_request_parameters_fail_without_account_identity_fallback_or_send() { - let store = Arc::new(MemoryAccountStore::default()); - create_account(&store, "acct_provider_contract").await; - let server = MockServer::start().await; - let provider = provider_with_base_url(&store, server.uri()); - for parameters in [ - json!({}), - json!({"installation_id":null}), - json!({"installation_id":42}), - ] { - let error = provider - .clone() - .execute( - planned_request("openai", http_generate_operation()), - context_with_middleware( - "req_invalid_parameters", - Arc::new(ParametersMiddleware( - parameters.as_object().unwrap().clone(), - )), - FastMode::Default, - ), - ) - .await - .err() - .expect("invalid parameters must fail"); - assert_eq!(error.kind(), ProviderErrorKind::InvalidRequest); - assert_eq!(error.send_state(), UpstreamSendState::NotSent); - assert!(error.retry_is_prohibited()); - } - assert!(server.received_requests().await.unwrap().is_empty()); - Mock::given(method("POST")) - .and(path("/codex/responses")) - .respond_with( - ResponseTemplate::new(200) - .insert_header("content-type", "text/event-stream") - .set_body_string(CAPTURE_COMPLETED_SSE), - ) - .expect(1) - .mount(&server) - .await; - let mut stream = provider - .execute( - planned_request("openai", http_generate_operation()), - context("req_after_invalid_parameters", CancellationToken::new()), - ) - .await - .unwrap(); - while let Some(event) = stream.next().await { - event.unwrap(); - } -} - fn metadata() -> Value { json!({"session_id":"source-session", "thread_id":"source-thread", "turn_id":"source-turn", "parent_thread_id":"source-session", "forked_from_thread_id":"source-session", diff --git a/backend/crates/providers/openai/tests/provider/live.rs b/backend/crates/providers/openai/tests/provider/live.rs index debd6f08d..aad39f4a7 100644 --- a/backend/crates/providers/openai/tests/provider/live.rs +++ b/backend/crates/providers/openai/tests/provider/live.rs @@ -9,8 +9,8 @@ use gateway_core::account::{ AccountModelAccess, AccountModelAccessMode, FastMode, ProviderAccountId, ProviderAccountStore, }; use gateway_core::engine::middleware::{ - MiddlewareContext, MiddlewareError, MiddlewareNext, MiddlewarePlan, MiddlewareRequest, - MiddlewareResponse, + MiddlewareContext, MiddlewareError, MiddlewareHeader, MiddlewareNext, MiddlewarePlan, + MiddlewareRequest, MiddlewareResponse, }; use gateway_core::engine::provider::{Provider, ProviderRequest}; use gateway_core::lifecycle::CancellationToken; @@ -202,28 +202,8 @@ async fn live_bootstrap_preserves_model_through_middleware_and_metadata() { upstream.verify().await; } -#[derive(Debug)] -struct ParametersMiddleware; - -impl MiddlewarePlan for ParametersMiddleware { - fn handle( - &self, - _context: MiddlewareContext, - request: MiddlewareRequest, - next: MiddlewareNext, - ) -> BoxFuture<'static, Result> { - assert!(request.provider_parameters().unwrap()["installation_id"].is_string()); - next.run( - request.with_provider_parameters(serde_json::Map::from_iter([( - "installation_id".into(), - json!("live-plugin-installation"), - )])), - ) - } -} - #[tokio::test] -async fn live_create_uses_request_parameters_without_changing_call_ownership() { +async fn live_create_uses_outbound_rewrite_without_changing_call_ownership() { let store = Arc::new(MemoryAccountStore::default()); create_account(&store, ACCOUNT).await; let server = MockServer::start().await; @@ -247,11 +227,16 @@ async fn live_create_uses_request_parameters_without_changing_call_ownership() { let mut stream = provider .execute( request, - context_with_middleware( - "req_live_parameters", - Arc::new(ParametersMiddleware), - FastMode::Default, - ), + super::contract::outbound_context("req_live_outbound", |request| { + request + .headers + .retain(|header| header.name() != "x-codex-turn-metadata"); + request.headers.push(MiddlewareHeader::new( + "x-codex-turn-metadata", + Bytes::from(json!({"installation_id":"live-plugin-installation"}).to_string()), + )); + Ok(()) + }), ) .await .unwrap(); diff --git a/backend/crates/providers/xai/src/provider/mod.rs b/backend/crates/providers/xai/src/provider/mod.rs index adebc33eb..bbe1e16ff 100644 --- a/backend/crates/providers/xai/src/provider/mod.rs +++ b/backend/crates/providers/xai/src/provider/mod.rs @@ -296,8 +296,7 @@ impl GrokBuildProvider { provider_kind, Some(model), account_id, - None, - Box::new(move |operation, middleware_headers, _| { + Box::new(move |operation, middleware_headers| { Box::pin(async move { if let Some(adapter) = adapter { return provider.execute_upstream_adapter( diff --git a/docs/api.md b/docs/api.md index 7c09f8d03..624b892b2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -316,8 +316,8 @@ Responses 也不透传 `x-stainless-*`、`Origin`、`Referer`、`sec-ch-ua*` 和 Images 的 `x-codex-image-turn-id` 和 Desktop 自动 Git 上下文的 `clientUserMessageId`。 输入项元数据保持不变。Search 的会话 `id` 按同一安装身份域映射,只替换该值的原始字节,其余正文保持不变。 `prompt_cache_key` 同样按安装身份映射,保留同一身份内的缓存共享关系;未提供时不补造。 -受信任插件可通过 attempt 构造参数选择本次安装身份;参数只影响出站构造,不改变实际选号与计费归属, -详见 [SDK 请求构造参数](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#模型请求与-attempt)。 +受信任插件可读取原始请求,自行生成身份并在 `outbound` 改写完整请求与关联字段;宿主不重新派生或补回业务字段, +实际选号与计费归属保持不变,详见 [SDK 出站请求准备](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#上游出站准备)。 续接 response ID、历史 item/call ID、业务正文和未知扩展不参与映射, 该处理减少显式追踪标识的跨账号复用,不保证会话内容或业务 metadata 无法关联 @@ -475,7 +475,7 @@ Images 排除已知为 Free 套餐的 OAuth 账号;套餐缺失或未知时不 `/v1/alpha/search` 使用相同的 OpenAI Provider 原生端点边界:模型不映射,正文除会话 `id` 的账号归一及显式请求策略外原样转发, Provider 旁路读取请求中的 `model`,按[账号模型限制](#账号模型限制)选号。 -`x-codex-turn-metadata` 在移除客户端账号身份并按本次构造参数重写 installation ID 后转发;上游账号 +`x-codex-turn-metadata` 在移除客户端账号身份并按选中账号重写 installation ID 后转发;上游账号 Authorization、Cookie、account ID、originator 和 User-Agent 均由代理安全重建。 Images 与 Search 保留通过现有过滤规则的普通业务请求头及多值字节;Responses 原连接的 turn state 不随独立端点转发,turn metadata 仍由所属端点按当前账号处理。 diff --git a/docs/architecture.md b/docs/architecture.md index cbfb0cea1..ae0837aa1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -172,8 +172,6 @@ flowchart LR 握手、socket 与断连由 API 持有,会话处理通过原 RPC 资源流保活;HTTP 返回链替换响应时释放未提交的会话。 原生与插件接管的下游 WebSocket 共用消息组合和取消入口,在默认解析前和实际写入前处理完整消息;读写并发进行,插件调用期间不占用 transport 写锁。 `request` 包裹逻辑模型请求,`attempt` 包裹每次已选号的执行;请求顺序进入、响应逆序返回。 - attempt 可携带 Provider 的本次构造参数,Core / Runtime 只传递完整替换后的对象,解释和缺失校验由 Provider 负责; - 参数不改变真实租约、凭据版本和计费归属,也不承载宿主对插件策略的推断、补值或持久化。 `outbound` 在原生默认值、隐私处理、序列化及内容压缩之后组合完整请求,续体只准备值,Provider 在整条返回链结束后发送。 无绑定或未命中当前请求范围时保留原生请求构造,不进入出站准备链。 它复用本次 attempt 的冻结计划、取消、期限和副作用水位;Provider 不再次应用业务默认值或重写正文,插件负责报文与内容编码的一致性。 @@ -467,7 +465,7 @@ OpenAI 模型目录用于发现,不因目录缺项拒绝请求;管理员配 - OpenAI 是透明边界。正文兼容、身份归一和显式请求策略之外的业务字段保留原始语义; 业务响应按原始字节转发,客户端错误兼容与原生续写额度恢复按明确例外处理,见 [OpenAI 透传](api.md#openai-透传)。 `transport/correlation` 在出站副本上按本次安装身份统一映射关联标识并剥离客户端 trace; - 安装身份默认取自账号,可由 attempt 的构造参数改写,缺失或非法结果不回退默认值。 + 原生安装身份取自账号;插件的身份生成、持久化及关联字段改写通过既有中间件链路自行完成。 内部选号、亲和、连接池与本地诊断继续读取原始身份;具体字段边界见 [API 协议](api.md#请求头与身份) canonical facts 从同一数据旁路提取,用于路由、恢复判断、观测和计费。 WebSocket transport 按完整文本消息交付,wire 同时保留原文与可选解析视图;同协议出口使用原文,