Skip to content

Commit 7ebd735

Browse files
committed
fix(codex): strip Responses-Lite private header upstream
Root cause: Codex Desktop sends x-openai-internal-codex-responses-lite to its local backend as an internal client/backend capability signal. CCSwitchMulti rebuilds request headers and previously default-passed unknown headers to the real upstream, so official ChatGPT Codex and third-party OpenAI-compatible upstreams could receive the private Lite switch. After OpenAI tightened that path for gpt-5.5, the upstream rejects requests with HTTP 400: This model is not supported when using X-OpenAI-Internal-Codex-Responses-Lite.\n\nFix the boundary in the proxy forwarder instead of changing MultiRouter, catalog schema, or provider UI. The header policy now strips only the known Codex Responses-Lite private header before default passthrough. OAuth account/session/cache headers keep their existing behavior, preserving the login-stability work from the v3.16.3-23 line and avoiding broad header scrubbing.\n\nThe same fix was first prepared for upstream cc-switch on branch codex/strip-codex-responses-lite-header and submitted as farion1231#4727.\n\nTests added in forwarder cover official chatgpt.com Codex upstream stripping, third-party upstream stripping, and ordinary header preservation. memory.md records the root cause and future regression boundary.\n\nVerified:\n- cargo fmt --manifest-path src-tauri\\Cargo.toml --check\n- cargo test --manifest-path src-tauri\\Cargo.toml codex_responses_lite_header --lib\n- cargo test --manifest-path src-tauri\\Cargo.toml ordinary_headers_are_preserved_for_upstream --lib\n- git diff --check
1 parent fa32a34 commit 7ebd735

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

memory.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# CC Switch Repository Memory
22

3+
## 2026-06-28 Codex Responses-Lite Header Upstream Strip
4+
5+
- `This model is not supported when using X-OpenAI-Internal-Codex-Responses-Lite` 的根因不是 MultiRouter 自身路由错误,而是 Codex Desktop 发给本地后端的内部协商头 `x-openai-internal-codex-responses-lite` 被 CC Switch / CCSwitchMulti 的 `forwarder.rs` 默认透传到了真实上游。OpenAI 在 2026-06-26 左右收紧 Lite 路径后,`gpt-5.5` 等模型会因此在 official ChatGPT Codex upstream 或第三方代理 upstream 返回 HTTP 400。
6+
- 正确修复边界在转发层 header policy:`src-tauri/src/proxy/forwarder.rs` 构建 `ordered_headers` 时,在默认透传前调用 `should_strip_codex_private_header_for_upstream()`,无条件移除 `x-openai-internal-codex-responses-lite`。不要把它修成 UI 开关、catalog schema、模型映射或 MultiRouter route 规则;也不要粗暴移除 OAuth/session/account headers,否则会破坏 Codex 官方登录态、前缀缓存和 CCSwitchMulti 之前的 OAuth login-preservation 修复。
7+
- 这次先在原版 `C:\Users\sunda\Documents\LLMservice\ccswitch official` 基于 `origin/main` 创建 `codex/strip-codex-responses-lite-header`,提交 `1e6a46b7 fix(proxy): strip Codex Responses-Lite header upstream`,并向 `farion1231/cc-switch` 提交 PR `#4727`,关联 issue `#4700`。随后把同一策略移植到 CCSwitchMulti `C:\Users\sunda\Documents\LLMservice\cc-switch``codex/merge-official-v3.16.4` 分支。
8+
- 回归测试落点:`proxy::forwarder::tests::codex_responses_lite_header_is_stripped_for_official_upstream``codex_responses_lite_header_is_stripped_for_third_party_upstream``ordinary_headers_are_preserved_for_upstream`。验证命令优先跑 `cargo fmt --manifest-path src-tauri\Cargo.toml --check``cargo test --manifest-path src-tauri\Cargo.toml codex_responses_lite_header --lib``cargo test --manifest-path src-tauri\Cargo.toml ordinary_headers_are_preserved_for_upstream --lib``git diff --check`
9+
310
## 2026-06-28 CCSwitchMulti v3.16.4-1 Prerelease
411

512
- `v3.16.4-1` 已作为 BigStrongSun/ccswitchmulti 的 GitHub prerelease 发布:`https://github.com/BigStrongSun/ccswitchmulti/releases/tag/v3.16.4-1`。Release 为非 draft、`prerelease=true`,发布时间为 `2026-06-27T20:52:24Z`,target commit 为 `e0228d531d1a7086a808d706e6ecb2618de44f4c``docs(memory): record completed v3.16.4-1 merge`)。

src-tauri/src/proxy/forwarder.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,6 +1976,13 @@ impl RequestForwarder {
19761976
continue;
19771977
}
19781978

1979+
// Codex Desktop 会给本地后端发送内部协商头。CCSwitchMulti 是上游反代,
1980+
// 不能把这些客户端私有信号继续透传,否则会让官方或第三方上游误入
1981+
// Responses-Lite 路径并拒绝部分模型。
1982+
if should_strip_codex_private_header_for_upstream(&url, key) {
1983+
continue;
1984+
}
1985+
19791986
// --- 认证类 — 用 adapter 提供的认证头替换(在原始位置) ---
19801987
if key_str.eq_ignore_ascii_case("authorization")
19811988
|| key_str.eq_ignore_ascii_case("x-api-key")
@@ -3012,6 +3019,21 @@ fn is_managed_account_upstream_url(url: &str) -> bool {
30123019
|| (host == "chatgpt.com" && uri.path().starts_with("/backend-api/codex"))
30133020
}
30143021

3022+
/// 判断某个 Codex 客户端私有头是否应该在转发到上游前移除。
3023+
///
3024+
/// `x-openai-internal-codex-responses-lite` 是 Codex Desktop 与本地后端之间的内部
3025+
/// 能力开关,不是公开 upstream API 契约。无论目标是官方 ChatGPT Codex 后端还是
3026+
/// 第三方 OpenAI-compatible 服务,都不应由 CCSwitchMulti 继续转发。
3027+
fn should_strip_codex_private_header_for_upstream(_url: &str, name: &http::HeaderName) -> bool {
3028+
is_codex_responses_lite_header(name)
3029+
}
3030+
3031+
/// 识别会触发上游 Responses-Lite 分支的 Codex 内部请求头。
3032+
fn is_codex_responses_lite_header(name: &http::HeaderName) -> bool {
3033+
name.as_str()
3034+
.eq_ignore_ascii_case("x-openai-internal-codex-responses-lite")
3035+
}
3036+
30153037
fn headers_contain_proxy_placeholder(headers: &http::HeaderMap) -> bool {
30163038
headers.values().any(|value| {
30173039
value
@@ -3447,6 +3469,39 @@ mod tests {
34473469
}
34483470
}
34493471

3472+
// 验证官方 Codex upstream 不会收到 Codex Desktop 的 Responses-Lite 内部开关。
3473+
#[test]
3474+
fn codex_responses_lite_header_is_stripped_for_official_upstream() {
3475+
let header = http::HeaderName::from_static("x-openai-internal-codex-responses-lite");
3476+
3477+
assert!(should_strip_codex_private_header_for_upstream(
3478+
"https://chatgpt.com/backend-api/codex/responses",
3479+
&header
3480+
));
3481+
}
3482+
3483+
// 验证第三方 OpenAI-compatible upstream 也不会收到官方客户端私有 header。
3484+
#[test]
3485+
fn codex_responses_lite_header_is_stripped_for_third_party_upstream() {
3486+
let header = http::HeaderName::from_static("x-openai-internal-codex-responses-lite");
3487+
3488+
assert!(should_strip_codex_private_header_for_upstream(
3489+
"https://api.example.com/v1/responses",
3490+
&header
3491+
));
3492+
}
3493+
3494+
// 验证过滤策略只移除已知 Codex 私有头,不影响普通自定义 header 透传。
3495+
#[test]
3496+
fn ordinary_headers_are_preserved_for_upstream() {
3497+
let header = http::HeaderName::from_static("x-custom-feature");
3498+
3499+
assert!(!should_strip_codex_private_header_for_upstream(
3500+
"https://chatgpt.com/backend-api/codex/responses",
3501+
&header
3502+
));
3503+
}
3504+
34503505
#[test]
34513506
fn single_provider_retryable_log_uses_single_provider_code() {
34523507
let error = ProxyError::UpstreamError {

0 commit comments

Comments
 (0)