diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 42eada9e3..ade9b9857 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -27,21 +27,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -100,18 +85,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "async-compression" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" -dependencies = [ - "compression-codecs", - "compression-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "async-lock" version = "3.4.2" @@ -661,27 +634,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -829,26 +781,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "compression-codecs" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" -dependencies = [ - "brotli", - "compression-core", - "flate2", - "memchr", - "zstd", - "zstd-safe", -] - -[[package]] -name = "compression-core" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" - [[package]] name = "config" version = "0.15.25" @@ -3922,17 +3854,12 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "async-compression", "bitflags", "bytes", - "futures-core", "futures-util", "http 1.4.2", "http-body 1.0.1", - "http-body-util", "pin-project-lite", - "tokio", - "tokio-util", "tower", "tower-layer", "tower-service", @@ -4732,31 +4659,3 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/backend/crates/gateway-api/src/openai/responses/request.rs b/backend/crates/gateway-api/src/openai/responses/request.rs index 8d6e77c1d..6a8635b0d 100644 --- a/backend/crates/gateway-api/src/openai/responses/request.rs +++ b/backend/crates/gateway-api/src/openai/responses/request.rs @@ -474,6 +474,11 @@ fn passthrough_header_name(name: &str, connection_headers: &[String]) -> bool { // 不能透传覆盖,避免不同下游客户端暴露不一致的设备指纹。 | "originator" | "user-agent" + // 设备 attestation/integrity 头只能由官方客户端或网关自身生成, + // 客户端注入的 x-oai-attestation / X-OAI-IS 不得透传上游。 + | "x-oai-attestation" + | "x-oai-is" + | "x-oai-is-update" ) } diff --git a/backend/crates/gateway-api/tests/openai/responses/mod.rs b/backend/crates/gateway-api/tests/openai/responses/mod.rs index 50a570651..f55eb3f74 100644 --- a/backend/crates/gateway-api/tests/openai/responses/mod.rs +++ b/backend/crates/gateway-api/tests/openai/responses/mod.rs @@ -288,6 +288,15 @@ fn decoder_should_preserve_ordinary_request_headers_as_opaque_multivalues() { "x-codex-installation-id", HeaderValue::from_static("client-installation"), ); + headers.insert( + "x-oai-attestation", + HeaderValue::from_static("client-attestation"), + ); + headers.insert("x-oai-is", HeaderValue::from_static("client-is")); + headers.insert( + "x-oai-is-update", + HeaderValue::from_static("client-is-update"), + ); let decoded = decode_request_with_headers(br#"{"model":"smart-code","input":"hello"}"#, &headers) @@ -340,6 +349,9 @@ fn decoder_should_preserve_ordinary_request_headers_as_opaque_multivalues() { // 上游指纹由运行时画像统一生成,客户端不得覆盖。 "user-agent", "originator", + "x-oai-attestation", + "x-oai-is", + "x-oai-is-update", ] { assert!(values(excluded).is_empty(), "unexpected {excluded}"); } diff --git a/backend/crates/providers/openai/Cargo.toml b/backend/crates/providers/openai/Cargo.toml index 2826d16da..9d8248713 100644 --- a/backend/crates/providers/openai/Cargo.toml +++ b/backend/crates/providers/openai/Cargo.toml @@ -28,7 +28,8 @@ hex = "0.4.3" # 会偏离官方指纹;升级前必须重新抓握手对比扩展顺序。 hyper-rustls = "=0.27.7" jsonwebtoken = { version = "=11.0.0", default-features = false, features = ["aws_lc_rs", "use_pem"] } -reqwest = { version = "=0.12.28", default-features = false, features = ["json", "stream", "cookies", "rustls-tls-native-roots", "gzip", "brotli", "zstd", "deflate", "http2"] } +# 与官方 Codex Desktop 一致:reqwest 不启用压缩特性,上游请求不带 accept-encoding。 +reqwest = { version = "=0.12.28", default-features = false, features = ["json", "stream", "cookies", "rustls-tls-native-roots", "http2"] } roxmltree = "0.21.1" semver = "1.0.27" # 保持 ring provider:reqwest rustls-tls-native-roots 即 __rustls-ring,与官方一致; diff --git a/backend/crates/providers/openai/src/transport/client.rs b/backend/crates/providers/openai/src/transport/client.rs index 3c8cfb943..20c4cd067 100644 --- a/backend/crates/providers/openai/src/transport/client.rs +++ b/backend/crates/providers/openai/src/transport/client.rs @@ -47,6 +47,7 @@ type ReqwestClientCache = Mutex>; /// 构建带缓存、自动协商 HTTP/2 的 reqwest Client。 pub fn build_reqwest_client() -> Result { + super::tls::ensure_rustls_provider(); let cache_key = custom_ca_env_cache_key(); static CLIENTS: OnceLock = OnceLock::new(); let cache = CLIENTS.get_or_init(|| Mutex::new(HashMap::new())); @@ -68,11 +69,7 @@ pub fn build_reqwest_client() -> Result { .tcp_keepalive(Duration::from_secs(30)) .http2_keep_alive_interval(Duration::from_secs(30)) .http2_keep_alive_timeout(Duration::from_secs(5)) - .http2_keep_alive_while_idle(true) - .gzip(true) - .brotli(true) - .zstd(true) - .deflate(true); + .http2_keep_alive_while_idle(true); let client = build_reqwest_client_with_custom_ca(builder)?; let mut clients = cache .lock() diff --git a/backend/crates/providers/openai/src/transport/client_sse.rs b/backend/crates/providers/openai/src/transport/client_sse.rs index 7ca1458c9..9f7e60b54 100644 --- a/backend/crates/providers/openai/src/transport/client_sse.rs +++ b/backend/crates/providers/openai/src/transport/client_sse.rs @@ -522,7 +522,10 @@ impl CodexBackendClient { for name in request.passthrough_headers.keys() { // 上游指纹必须由运行时画像统一生成:originator/User-Agent 即使绕过 // API 透传黑名单也不能覆盖画像,避免下游客户端暴露不一致指纹。 - if matches!(name.as_str(), "originator" | "user-agent") { + if matches!( + name.as_str(), + "originator" | "user-agent" | "x-oai-attestation" | "x-oai-is" | "x-oai-is-update" + ) { continue; } headers.remove(name); diff --git a/backend/crates/providers/openai/tests/transport/headers.rs b/backend/crates/providers/openai/tests/transport/headers.rs index 4c78fe283..2f9d19cd0 100644 --- a/backend/crates/providers/openai/tests/transport/headers.rs +++ b/backend/crates/providers/openai/tests/transport/headers.rs @@ -48,7 +48,10 @@ fn request_with_opaque_headers(use_websocket: bool) -> CodexResponsesRequest { [ "x-codex-installation-id", STANDARD.encode(b"client-installation") - ] + ], + ["x-oai-attestation", STANDARD.encode(b"client-attestation")], + ["x-oai-is", STANDARD.encode(b"client-is")], + ["x-oai-is-update", STANDARD.encode(b"client-is-update")] ]), ), ("turn_state".to_owned(), json!("typed-turn-state")), @@ -413,6 +416,12 @@ async fn backend_http_should_restore_opaque_multivalue_header_bytes_and_lease_id raw_header_values(&raw, "originator"), vec![b"codex_cli_rs".to_vec()] ); + for dropped in ["x-oai-attestation", "x-oai-is", "x-oai-is-update"] { + assert!( + raw_header_values(&raw, dropped).is_empty(), + "unexpected {dropped}" + ); + } for secret in [ b"client-secret".as_slice(), b"client-account", @@ -515,6 +524,9 @@ async fn backend_websocket_should_drop_only_unrepresentable_opaque_header_values vec![test_wire_profile().snapshot().user_agent().into_bytes()] ); assert_eq!(values("originator"), vec![b"codex_cli_rs".to_vec()]); + for dropped in ["x-oai-attestation", "x-oai-is", "x-oai-is-update"] { + assert!(values(dropped).is_empty(), "unexpected {dropped}"); + } assert_eq!( values("authorization"), vec![b"Bearer lease-token".to_vec()] diff --git a/backend/crates/providers/xai/Cargo.toml b/backend/crates/providers/xai/Cargo.toml index d480b8d64..feccb88f6 100644 --- a/backend/crates/providers/xai/Cargo.toml +++ b/backend/crates/providers/xai/Cargo.toml @@ -19,7 +19,8 @@ gateway-core = { path = "../../gateway-core" } gateway-protocol = { path = "../../gateway-protocol" } getrandom = "0.4.3" jsonwebtoken = { version = "=11.0.0", default-features = false, features = ["aws_lc_rs"] } -reqwest = { version = "=0.12.28", default-features = false, features = ["stream", "rustls-tls-native-roots", "gzip", "brotli", "zstd", "deflate", "http2"] } +# 与官方 Grok CLI 一致:reqwest 不启用压缩特性,上游请求不带 accept-encoding。 +reqwest = { version = "=0.12.28", default-features = false, features = ["stream", "rustls-tls-native-roots", "http2"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.150", features = ["arbitrary_precision", "preserve_order"] } semver = "1.0.27" diff --git a/backend/crates/providers/xai/src/transport/headers.rs b/backend/crates/providers/xai/src/transport/headers.rs index 6b738dde6..fbb06286b 100644 --- a/backend/crates/providers/xai/src/transport/headers.rs +++ b/backend/crates/providers/xai/src/transport/headers.rs @@ -127,7 +127,6 @@ pub fn build_grok_headers( GrokHeader::public("user-agent", profile.user_agent()), GrokHeader::public("content-type", "application/json"), GrokHeader::public("accept", "text/event-stream"), - GrokHeader::public("accept-encoding", "identity"), GrokHeader::sensitive("x-grok-req-id", SecretValue::new(upstream_request_id)), GrokHeader::sensitive( "idempotency-key", diff --git a/backend/crates/providers/xai/tests/transport/headers.rs b/backend/crates/providers/xai/tests/transport/headers.rs index e3c7ba496..b3e0cb1c4 100644 --- a/backend/crates/providers/xai/tests/transport/headers.rs +++ b/backend/crates/providers/xai/tests/transport/headers.rs @@ -71,7 +71,7 @@ fn headers_should_bind_identity_to_the_selected_oauth_account() { assert_eq!(value("x-grok-client-version"), Some("0.2.106")); assert_eq!(value("x-grok-client-identifier"), Some("grok-shell")); assert_eq!(value("x-grok-client-mode"), Some("headless")); - assert_eq!(value("accept-encoding"), Some("identity")); + assert_eq!(value("accept-encoding"), None); assert_eq!(value("x-grok-turn-idx"), Some("7")); assert_eq!( value("user-agent"),