fix(openai): preserve deployment account routing policy

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-11-03 11:33:53 +08:00
co-authored by Codex
parent 3b4b547e9c
commit e7c24781fb
9 changed files with 41 additions and 45 deletions
@@ -32,7 +32,6 @@ impl CodexBackendClient {
content_type: Option<&str>,
) -> CodexClientResult<HeaderMap> {
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,
@@ -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"
@@ -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()]
@@ -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::<Vec<_>>(),
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::<Vec<_>>(),
["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;
@@ -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"),
@@ -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::<Vec<_>>(),
["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",
] {
@@ -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::<Vec<_>>()
};
assert_eq!(
headers
.get_all(name)
.iter()
.map(|value| value.to_str().unwrap())
.collect::<Vec<_>>(),
expected_values,
value.into_iter().collect::<Vec<_>>(),
"{document}, websocket={websocket}, downstream={has_downstream_headers}, {name}",
);
}
+3 -4
View File
@@ -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 互联
+4 -4
View File
@@ -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)