codex: forward body version

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-06-15 11:34:54 +08:00
co-authored by Codex
parent 4202a31f69
commit 56d8476275
3 changed files with 35 additions and 15 deletions
+4 -3
View File
@@ -249,7 +249,8 @@ Rust 证据:
- 本轮修正后,`src/codex/serving/dispatch/mod.rs` 的 `stagger_request_with_deps()` 会在同账号 previous slot 存在时按 `request_interval_ms` 的 ±30% jitter 计算等待时间;`tests/codex_serving/responses_http_sse.rs` 覆盖同账号并发请求发送上游前会等待,`jitter_request_interval_ms_with_factor_should_match_original_bounds` 覆盖 0.7/1.3 边界。
- 本轮修正后,`src/codex/gateway/transport/http_client.rs` 的 Rust reqwest client 按 `force_http11` 缓存复用,使用 `reqwest = 0.12.28`、`rustls = 0.23.36`,启用 rustls/native roots、`pool_max_idle_per_host(4)`、`tcp_keepalive(30s)`、gzip/brotli/zstd/deflate,并可选 `http1_only()`;同时显式 `.no_proxy()`。
- `tests/codex_gateway/http_client.rs:13-78` 覆盖 HTTP SSE POST、desktop headers、Cookie 注入、`Set-Cookie` 捕获、turn state 捕获和 usage 提取。
- `tests/codex_serving/responses_http_sse.rs:403-513` 覆盖 `/v1/responses` HTTP SSE 路径的 context headers、account-scoped prompt cache key、metadata 与 service tier。
- 本轮修正后,`src/codex/serving/responses.rs` 会按原版从 body 读取 `version`,并以 body 优先、header 兜底的顺序写入上游 `version` header;本地字段仍不会进入上游 JSON body。
- `tests/codex_serving/responses_http_sse.rs::v1_responses_should_forward_parity_fields_and_context_headers` 覆盖 `/v1/responses` HTTP SSE 路径的 context headers、body control 字段、account-scoped prompt cache key、metadata、subagent、service tier,以及 `turnState`/`turnMetadata`/`betaFeatures`/`includeTimingMetrics`/`version`/`codexWindowId`/`parentThreadId`/`use_websocket` 不泄漏到上游 body。
结论:部分对齐。
@@ -262,6 +263,7 @@ Rust 证据:
- Rust 与原版 native transport 使用同一 reqwest/rustls 主版本组合,且都支持 `force_http11`。
- `/v1/responses/compact` 已对齐原版 JSON 上游链路:URL/method、核心 headers、Cookie/Set-Cookie、非 SSE Accept、body 字段白名单、reasoning/text 清洗和 JSON 响应透传均有实现;`v1_responses_compact_should_post_json_to_codex_compact_upstream` 覆盖该路径。
- HTTP SSE 与 compact 请求的真实 wire header 相对顺序已由 raw TCP 测试覆盖,不再只是字段级断言。
- HTTP SSE golden test 已覆盖 body control 字段转 header/metadata、账号作用域 identity,以及本地控制字段不进入上游 JSON body。
- IP 代理/VPN/proxy config/HttpsProxyAgent 是用户明确排除的非目标。Rust `.no_proxy()` 与原版 proxy 支持的差异不计入本项目 100% 一致性缺口,但需要保留边界说明。
未完全对齐:
@@ -269,8 +271,7 @@ Rust 证据:
- 原版 native client builder 没有显式 `.no_proxy()`,而是通过 proxy 参数控制;Rust 显式 `.no_proxy()` 符合非代理目标,但也意味着环境代理不会生效。此项按非目标处理,不算 OpenAI 链路缺口。
缺口/后续动作:
- 增加 HTTP SSE golden test:断言上游 body 不包含本地控制字段,header/body 中 account-scoped identity、metadata、subagent、turn/context 字段与原版一致。
- 若要宣称安全模拟 100%,还需要处理 WS handshake header order、header casing、TLS 指纹和客户端断开 abort 语义;当前 HTTP SSE/compact order 已有 wire-level 证据。
- 若要宣称安全模拟 100%,还需要处理 TLS 指纹和客户端断开 abort 语义;当前 HTTP SSE/compact order 与 HTTP SSE body golden 已有 wire-level/端到端证据。
## 6. WebSocket 上游链路与连接池
+4 -1
View File
@@ -133,7 +133,7 @@ impl ResponsesService {
codex_request.include_timing_metrics = body
.include_timing_metrics
.or_else(|| header_string(&headers, "x-responsesapi-include-timing-metrics"));
codex_request.version = header_string(&headers, "version");
codex_request.version = body.version.or_else(|| header_string(&headers, "version"));
codex_request.codex_window_id = body
.codex_window_id
.or_else(|| header_string(&headers, "x-codex-window-id"));
@@ -934,6 +934,7 @@ struct ResponsesBody {
turn_state: Option<String>,
turn_metadata: Option<String>,
beta_features: Option<String>,
version: Option<String>,
include_timing_metrics: Option<String>,
codex_window_id: Option<String>,
parent_thread_id: Option<String>,
@@ -975,6 +976,7 @@ fn parse_responses_body(body: &[u8], default_model: String) -> Option<ResponsesB
parsed.turn_state = take_non_empty_string(&mut body, "turnState");
parsed.turn_metadata = take_non_empty_string(&mut body, "turnMetadata");
parsed.beta_features = take_non_empty_string(&mut body, "betaFeatures");
parsed.version = take_non_empty_string(&mut body, "version");
parsed.include_timing_metrics = take_non_empty_string(&mut body, "includeTimingMetrics");
parsed.codex_window_id = take_non_empty_string(&mut body, "codexWindowId");
parsed.parent_thread_id = take_non_empty_string(&mut body, "parentThreadId");
@@ -1031,6 +1033,7 @@ fn default_body(default_model: String) -> ResponsesBody {
turn_state: None,
turn_metadata: None,
beta_features: None,
version: None,
include_timing_metrics: None,
codex_window_id: None,
parent_thread_id: None,
+27 -11
View File
@@ -625,11 +625,11 @@ async fn v1_responses_should_forward_parity_fields_and_context_headers() {
.and(header("authorization", "Bearer access-secret"))
.and(header("chatgpt-account-id", "chatgpt-account"))
.and(header("x-codex-turn-state", "turn-body"))
.and(header("x-codex-turn-metadata", "meta-header"))
.and(header("x-codex-beta-features", "beta-header"))
.and(header("x-codex-turn-metadata", "meta-body"))
.and(header("x-codex-beta-features", "beta-body"))
.and(header("x-responsesapi-include-timing-metrics", "true"))
.and(header("version", "2026-06-12"))
.and(header("x-codex-parent-thread-id", "parent-1"))
.and(header("x-codex-parent-thread-id", "parent-body"))
.and(header("x-openai-subagent", "review"))
.respond_with(
ResponseTemplate::new(200)
@@ -654,10 +654,8 @@ async fn v1_responses_should_forward_parity_fields_and_context_headers() {
.header("x-codex-turn-state", "turn-header")
.header("x-codex-turn-metadata", "meta-header")
.header("x-codex-beta-features", "beta-header")
.header("x-responsesapi-include-timing-metrics", "true")
.header("version", "2026-06-12")
.header("x-codex-window-id", "window-1")
.header("x-codex-parent-thread-id", "parent-1")
.header("x-codex-window-id", "window-header")
.header("x-codex-parent-thread-id", "parent-header")
.header("x-openai-subagent", "review")
.body(Body::from(
json!({
@@ -685,6 +683,12 @@ async fn v1_responses_should_forward_parity_fields_and_context_headers() {
"safe": "yes"
},
"turnState": "turn-body",
"turnMetadata": "meta-body",
"betaFeatures": "beta-body",
"includeTimingMetrics": "true",
"version": "2026-06-12",
"codexWindowId": "window-body",
"parentThreadId": "parent-body",
"use_websocket": false
})
.to_string(),
@@ -700,7 +704,8 @@ async fn v1_responses_should_forward_parity_fields_and_context_headers() {
let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 1);
let upstream_body: Value = serde_json::from_slice(&requests[0].body).unwrap();
let identity = build_conversation_identity(Some("pcache"), Some("window-1"), "acct_imported");
let identity =
build_conversation_identity(Some("pcache"), Some("window-body"), "acct_imported");
assert_eq!(
upstream_body["prompt_cache_key"],
identity.conversation_id.unwrap()
@@ -718,17 +723,28 @@ async fn v1_responses_should_forward_parity_fields_and_context_headers() {
);
assert_eq!(
upstream_body["client_metadata"]["x-codex-turn-metadata"],
"meta-header"
"meta-body"
);
assert_eq!(
upstream_body["client_metadata"]["x-codex-parent-thread-id"],
"parent-1"
"parent-body"
);
let installation_id = upstream_body["client_metadata"]["x-codex-installation-id"]
.as_str()
.unwrap();
assert!(uuid::Uuid::parse_str(installation_id).is_ok());
assert!(upstream_body.get("use_websocket").is_none());
for local_field in [
"turnState",
"turnMetadata",
"betaFeatures",
"includeTimingMetrics",
"version",
"codexWindowId",
"parentThreadId",
"use_websocket",
] {
assert!(upstream_body.get(local_field).is_none());
}
}
#[tokio::test]