diff --git a/backend/crates/providers/openai/src/transport/client_live.rs b/backend/crates/providers/openai/src/transport/client_live.rs index e7e8b3085..3015ce02c 100644 --- a/backend/crates/providers/openai/src/transport/client_live.rs +++ b/backend/crates/providers/openai/src/transport/client_live.rs @@ -32,7 +32,6 @@ impl CodexBackendClient { content_type: Option<&str>, ) -> CodexClientResult { let mut headers = self.model_request_headers(&self.profile.snapshot(), context)?; - let mut passthrough = HeaderMap::new(); for header in extra_protocol_headers { let name = HeaderName::from_bytes(header.name().as_bytes())?; if is_managed_identity_header(name.as_str()) @@ -58,9 +57,8 @@ impl CodexBackendClient { } else { HeaderValue::from_bytes(header.value())? }; - passthrough.append(name, value); + headers.append(name, value); } - headers.extend(passthrough); if let Some(content_type) = content_type { headers.insert( reqwest::header::CONTENT_TYPE, diff --git a/backend/crates/providers/openai/src/transport/headers.rs b/backend/crates/providers/openai/src/transport/headers.rs index dd5f38c53..06e7b5682 100644 --- a/backend/crates/providers/openai/src/transport/headers.rs +++ b/backend/crates/providers/openai/src/transport/headers.rs @@ -34,7 +34,9 @@ pub(super) fn is_managed_identity_header(name: &str) -> bool { | "originator" | "user-agent" | "version" + // 地域和账号路由统一按部署配置生成 | "x-openai-internal-codex-residency" + | "x-openai-account-routing-override" | "x-oai-attestation" | "x-oai-is" | "x-oai-is-update" diff --git a/backend/crates/providers/openai/tests/compat/headers.rs b/backend/crates/providers/openai/tests/compat/headers.rs index 721aac510..fe7251312 100644 --- a/backend/crates/providers/openai/tests/compat/headers.rs +++ b/backend/crates/providers/openai/tests/compat/headers.rs @@ -45,7 +45,6 @@ const DOWNSTREAM_CLIENT_HEADERS: &[(&str, &str)] = &[ ]; const BUSINESS_EXTENSION_HEADERS: &[(&str, &str)] = &[ - ("X-OpenAI-Account-Routing-Override", "first-route"), ("x-openai-fedramp", "true"), ("x-grok-turn-idx", "7"), ("X-Grok-Model-Override", "synthetic-model"), @@ -79,6 +78,10 @@ fn request_with_opaque_headers(use_websocket: bool) -> CodexResponsesRequest { "x-openai-internal-codex-residency", STANDARD.encode(b"future-region") ], + [ + "X-OpenAI-Account-Routing-Override", + STANDARD.encode(b"client-route") + ], ["x-codex-turn-state", STANDARD.encode(b"turn-ascii")], ["x-codex-turn-state", STANDARD.encode(b"turn-\x80")], ["bad header name", STANDARD.encode(b"ignored")], @@ -286,6 +289,10 @@ async fn backend_http_should_preserve_business_headers_without_downstream_transp raw_header_values(&raw, "traceparent"), vec![b"synthetic-trace".to_vec()] ); + assert_eq!( + raw_header_values(&raw, "x-openai-account-routing-override"), + vec![b"configured-route".to_vec()] + ); assert_eq!( raw_header_values(&raw, "x-codex-installation-id"), vec![b"lease-installation".to_vec()] @@ -475,6 +482,10 @@ async fn backend_websocket_should_preserve_business_headers_without_downstream_t assert_eq!(values(name), vec![value.to_vec()], "missing {name}"); } assert_eq!(values("traceparent"), vec![b"synthetic-trace".to_vec()]); + assert_eq!( + values("x-openai-account-routing-override"), + vec![b"configured-route".to_vec()] + ); assert_eq!( values("x-codex-installation-id"), vec![b"lease-installation".to_vec()] diff --git a/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs b/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs index f61705b35..668011983 100644 --- a/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs +++ b/backend/crates/providers/openai/tests/provider/contract/account_isolation.rs @@ -71,20 +71,15 @@ fn assert_isolated_request( body: &Value, owner: Option<&str>, ) { - for (name, values) in [ - ("x-openai-account-routing-override", ["us", "us_cr"]), - ("x-openai-fedramp", ["true", "false"]), - ] { - assert_eq!( - headers - .get_all(name) - .iter() - .map(|value| value.to_str().unwrap()) - .collect::>(), - values, - "changed {name} with owner {owner:?}" - ); - } + assert!(!headers.contains_key("x-openai-account-routing-override")); + assert_eq!( + headers + .get_all("x-openai-fedramp") + .iter() + .map(|value| value.to_str().unwrap()) + .collect::>(), + ["true", "false"] + ); assert_eq!(headers["x-codex-guardian"], "true"); assert_eq!( headers @@ -120,7 +115,7 @@ fn assert_isolated_request( } #[tokio::test] -async fn http_preserves_routing_headers_and_only_scopes_guardian_parent_reference() { +async fn http_applies_deployment_routing_and_only_scopes_guardian_parent_reference() { for owner in [Some(SELECTED_ACCOUNT), Some("acct_scope_old"), None] { let store = Arc::new(MemoryAccountStore::default()); create_account(&store, SELECTED_ACCOUNT).await; @@ -163,7 +158,7 @@ async fn http_preserves_routing_headers_and_only_scopes_guardian_parent_referenc } #[tokio::test] -async fn websocket_preserves_routing_headers_and_only_scopes_guardian_parent_reference() { +async fn websocket_applies_deployment_routing_and_only_scopes_guardian_parent_reference() { for owner in [Some(SELECTED_ACCOUNT), Some("acct_scope_old"), None] { let store = Arc::new(MemoryAccountStore::default()); create_account(&store, SELECTED_ACCOUNT).await; diff --git a/backend/crates/providers/openai/tests/provider/contract/passthrough.rs b/backend/crates/providers/openai/tests/provider/contract/passthrough.rs index 4ed21f8c3..7c5b7c6dd 100644 --- a/backend/crates/providers/openai/tests/provider/contract/passthrough.rs +++ b/backend/crates/providers/openai/tests/provider/contract/passthrough.rs @@ -142,8 +142,12 @@ async fn independent_endpoints_send_opaque_headers_without_client_identity() { let sent = requests.last().unwrap(); assert_eq!(sent.url.path(), endpoint); uuid::Uuid::parse_str(sent.headers["x-codex-installation-id"].to_str().unwrap()).unwrap(); + assert!( + !sent + .headers + .contains_key("x-openai-account-routing-override") + ); for (name, expected) in [ - ("x-openai-account-routing-override", "client-route"), ("x-openai-fedramp", "true"), ("traceparent", "client-trace"), ("tracestate", "vendor=client"), diff --git a/backend/crates/providers/openai/tests/provider/live.rs b/backend/crates/providers/openai/tests/provider/live.rs index ca2d6bf84..61a5659b2 100644 --- a/backend/crates/providers/openai/tests/provider/live.rs +++ b/backend/crates/providers/openai/tests/provider/live.rs @@ -457,14 +457,6 @@ fn assert_live_correlation(headers: &reqwest::header::HeaderMap) { assert_eq!(headers["thread-id"], session); assert_eq!(headers["openai-alpha"], "live-test"); assert_eq!(headers["x-openai-fedramp"], "true"); - assert_eq!( - headers - .get_all("x-openai-account-routing-override") - .iter() - .map(|value| value.to_str().unwrap()) - .collect::>(), - ["first-route", "second-route"] - ); assert_eq!( headers .get_all("x-test-business") @@ -478,6 +470,7 @@ fn assert_live_correlation(headers: &reqwest::header::HeaderMap) { "x-oai-attestation", "x-forwarded-for", "x-stainless-runtime", + "x-openai-account-routing-override", "x-codex-turn-state", "x-codex-routing-hint", ] { diff --git a/backend/crates/providers/openai/tests/transport/profile_contract.rs b/backend/crates/providers/openai/tests/transport/profile_contract.rs index 5504aa54d..834d3e68e 100644 --- a/backend/crates/providers/openai/tests/transport/profile_contract.rs +++ b/backend/crates/providers/openai/tests/transport/profile_contract.rs @@ -143,7 +143,7 @@ fn deployment_routing_survives_artifact_updates_and_profile_selection() { } #[tokio::test] -async fn model_transports_apply_residency_and_client_or_default_routing() { +async fn model_transports_apply_deployment_routing_with_or_without_client_headers() { use base64::{Engine as _, engine::general_purpose::STANDARD}; use gateway_core::operation::{GenerateRequest, ProtocolPayload}; @@ -266,19 +266,13 @@ async fn model_transports_apply_residency_and_client_or_default_routing() { .expect("model response"); let headers = capture.await; for (name, value) in HEADERS.into_iter().zip(expected) { - let expected_values = - if name == "x-openai-account-routing-override" && has_downstream_headers { - vec!["downstream-first", "downstream-second"] - } else { - value.into_iter().collect::>() - }; assert_eq!( headers .get_all(name) .iter() .map(|value| value.to_str().unwrap()) .collect::>(), - expected_values, + value.into_iter().collect::>(), "{document}, websocket={websocket}, downstream={has_downstream_headers}, {name}", ); } diff --git a/deploy/README.md b/deploy/README.md index f8a2ebbe3..4b10eea44 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -392,10 +392,9 @@ PostgreSQL/Redis 启动密码。日常校验使用 `config --quiet` 位置覆盖只影响 OpenAI 请求中受支持的位置与时区信息,不改变上下文日期、真实出口 IP、epoch 时间戳或系统时区。 `openai.residency` 和 `openai.account_routing_override` 是独立于客户端身份的启动配置, 分别指定 `x-openai-internal-codex-residency` 和 `x-openai-account-routing-override` 请求头。 -两项均为可选字符串,不校验地域或路由枚举。 -地域头使用配置值,省略或设为 `null` 时不发送。 -路由头优先透传客户端值,客户端未提供时使用配置值,两者均缺省时不发送。 -示例配置均填写 `us`,修改后重启应用生效。身份与位置字段见 [运行设置 API](../docs/api.md#8-运行设置) +两项均为可选字符串,配置值原样发送并覆盖客户端同名头,支持自定义地域或路由值。 +省略或设为 `null` 时删除对应请求头。 +修改后重启应用生效。身份与位置字段见 [运行设置 API](../docs/api.md#8-运行设置) 后台账号操作使用 Provider 自己的官方画像;发布资料刷新不改写用户选择或 `config.yaml` ### CPR 互联 diff --git a/docs/api.md b/docs/api.md index 90181fcaa..c7ba2e0bc 100644 --- a/docs/api.md +++ b/docs/api.md @@ -396,10 +396,10 @@ API Key 与 OAuth 共用模拟客户端画像(`User-Agent`、`originator`、`v 上游认证只来自选中的账号;API Key 不携带 OAuth Cookie、ChatGPT 账号身份或下游的 `X-OpenAI-Actor-Authorization` 托管认证声明。 `x-codex-installation-id` 使用选中账号已生成并持久化的安装 ID,与正文 `client_metadata` 中的安装身份一致。 -`x-openai-internal-codex-residency` 仅按部署配置 `openai.residency` 生成,省略或 `null` 时不发送。 -`x-openai-account-routing-override` 和 `x-openai-fedramp` 按业务头透传,保留原始多值。 -客户端未提供路由头时,使用 `openai.account_routing_override` 配置值,配置说明见 -[部署与运维](../deploy/README.md#启动后的运行设置) +`x-openai-internal-codex-residency` 和 `x-openai-account-routing-override` 分别由部署配置 +`openai.residency`、`openai.account_routing_override` 控制,配置值原样发送并覆盖客户端同名头, +省略或 `null` 时删除对应请求头。配置说明见 [部署与运维](../deploy/README.md#启动后的运行设置) +`x-openai-fedramp` 按业务头透传,保留原始多值。 上述过滤描述原生转发;已信任插件的显式 header 改写可以覆盖账号与画像生成的默认头, 不按认证或会话字段名称拦截,边界见 [SDK 请求中间件](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#模型请求与-attempt)