From 5c39dfbfbea26f1a6d7d6838091d87e14bbfc17b Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Jul 2026 23:20:55 +0800 Subject: [PATCH] feat(codex): declare gpt-5.6 context window for Claude Code takeover - Update Codex OAuth presets to the gpt-5.6 family (haiku -> gpt-5.6-luna) and bump the custom Codex template default model - Inject CLAUDE_CODE_MAX_CONTEXT_TOKENS / CLAUDE_CODE_AUTO_COMPACT_WINDOW (372000, the ChatGPT Codex catalog window with a ~353K effective budget, openai/codex#31860) into effective live settings so Claude Code stops assuming a 200K window and compacts before the upstream rejects the prompt - Gate the injected defaults on every configured model being gpt-5.6*: gpt-5.5's upstream catalog oscillates between 272K and 372K and must not inherit them; explicit user values always win - Strip the injected values on switch-away backfill (mirror-inverse of the injection conditions) so program defaults never harden into per-provider explicit values, and keep both keys out of the shared common-config snippet --- src-tauri/src/services/provider/live.rs | 375 +++++++++++++++++++++ src-tauri/src/services/provider/mod.rs | 13 + src/config/claudeDesktopProviderPresets.ts | 2 +- src/config/claudeProviderPresets.ts | 30 +- src/config/codexTemplates.ts | 2 +- tests/config/claudeProviderPresets.test.ts | 42 +++ 6 files changed, 458 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 7b3451ae9..a37e17c5a 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -21,6 +21,107 @@ use super::gemini_auth::{ }; use super::normalize_claude_models_in_value; +/// ChatGPT Codex catalogs gpt-5.6 at a 372K context window with a ~353K +/// effective budget (openai/codex#31860), far below the 1.05M API spec. +/// Declare the catalog window for both knobs: Claude Code's built-in output +/// reserve and compact buffer already keep the actual compact trigger +/// (~278K-339K) below the effective budget, so anything lower only wastes +/// usable context. +const CODEX_OAUTH_CLAUDE_MAX_CONTEXT_TOKENS: &str = "372000"; +const CODEX_OAUTH_CLAUDE_AUTO_COMPACT_WINDOW: &str = "372000"; + +/// Model env keys Claude Code may route requests through. The defaults above +/// are calibrated against gpt-5.6's Codex catalog, so every configured model +/// must belong to that family before they are injected — gpt-5.5's upstream +/// catalog oscillates between 272K and 372K and must not inherit them. +const CODEX_OAUTH_MODEL_ENV_KEYS: [&str; 6] = [ + "ANTHROPIC_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_FABLE_MODEL", + "CLAUDE_CODE_SUBAGENT_MODEL", +]; + +fn provider_env_targets_gpt56(provider_env: Option<&serde_json::Map>) -> bool { + let Some(env) = provider_env else { + return false; + }; + let mut saw_model = false; + for key in CODEX_OAUTH_MODEL_ENV_KEYS { + let Some(value) = env.get(key) else { + continue; + }; + let Some(model) = value.as_str() else { + return false; + }; + let model = model.trim(); + if model.is_empty() { + continue; + } + saw_model = true; + if !model.to_ascii_lowercase().starts_with("gpt-5.6") { + return false; + } + } + saw_model +} + +/// Claude Code assigns unknown non-Claude model ids a 200K context window. +/// Codex OAuth deliberately exposes GPT ids through Claude Code, so enrich the +/// effective live settings for both newly-created and already-saved providers. +/// Explicit user values always win; the defaults are only injected when every +/// configured model targets gpt-5.6. +fn apply_codex_oauth_claude_context_defaults(settings: &mut Value, provider: &Provider) { + if !provider.is_codex_oauth() { + return; + } + + // Read provider-owned values before mutably borrowing the effective + // settings. This also deliberately prevents a legacy common-config + // snippet from overriding model-specific context limits. + let provider_env = provider + .settings_config + .get("env") + .and_then(Value::as_object); + let Some(root) = settings.as_object_mut() else { + return; + }; + let env = root.entry("env".to_string()).or_insert_with(|| json!({})); + let Some(env) = env.as_object_mut() else { + log::warn!( + "Cannot apply Codex OAuth Claude context defaults for '{}': env is not an object", + provider.id + ); + return; + }; + + let inject_defaults = provider_env_targets_gpt56(provider_env); + for (key, default_value) in [ + ( + "CLAUDE_CODE_MAX_CONTEXT_TOKENS", + CODEX_OAUTH_CLAUDE_MAX_CONTEXT_TOKENS, + ), + ( + "CLAUDE_CODE_AUTO_COMPACT_WINDOW", + CODEX_OAUTH_CLAUDE_AUTO_COMPACT_WINDOW, + ), + ] { + match provider_env.and_then(|provider_env| provider_env.get(key)) { + Some(value) => { + env.insert(key.to_string(), value.clone()); + } + None if inject_defaults => { + env.insert(key.to_string(), Value::String(default_value.to_string())); + } + // 老模型不注入默认值,同时剥掉遗留共享片段可能带进来的值 + None => { + env.remove(key); + } + } + } +} + pub(crate) fn sanitize_claude_settings_for_live(settings: &Value) -> Value { let mut v = settings.clone(); if let Some(obj) = v.as_object_mut() { @@ -537,6 +638,10 @@ pub(crate) fn build_effective_settings_with_common_config( } } + if matches!(app_type, AppType::Claude) { + apply_codex_oauth_claude_context_defaults(&mut effective_settings, provider); + } + Ok(effective_settings) } @@ -604,11 +709,55 @@ pub(crate) fn strip_common_config_from_live_settings( restore_live_settings_for_provider_backfill(app_type, provider, backfill_settings) } +/// 与 `apply_codex_oauth_claude_context_defaults` 严格对称:注入产物只活在 +/// live,切走回填时必须剥掉,否则程序默认值会固化成供应商的"用户显式值", +/// 之后调整默认值或更换模型时旧值永远压住新默认。仅当"注入会发生且注入的 +/// 就是这个值、且存储配置本来没有显式值"时才剥;用户显式存储的值和手改 +/// live 成其他数字的值都保留。 +fn strip_injected_codex_oauth_context_defaults(settings: &mut Value, provider: &Provider) { + if !provider.is_codex_oauth() { + return; + } + let provider_env = provider + .settings_config + .get("env") + .and_then(Value::as_object); + if !provider_env_targets_gpt56(provider_env) { + return; + } + let Some(env) = settings.get_mut("env").and_then(Value::as_object_mut) else { + return; + }; + for (key, default_value) in [ + ( + "CLAUDE_CODE_MAX_CONTEXT_TOKENS", + CODEX_OAUTH_CLAUDE_MAX_CONTEXT_TOKENS, + ), + ( + "CLAUDE_CODE_AUTO_COMPACT_WINDOW", + CODEX_OAUTH_CLAUDE_AUTO_COMPACT_WINDOW, + ), + ] { + let stored_explicit = provider_env.is_some_and(|e| e.contains_key(key)); + if stored_explicit { + continue; + } + if env.get(key).and_then(Value::as_str) == Some(default_value) { + env.remove(key); + } + } +} + fn restore_live_settings_for_provider_backfill( app_type: &AppType, provider: &Provider, live_settings: Value, ) -> Value { + if matches!(app_type, AppType::Claude) { + let mut settings = live_settings; + strip_injected_codex_oauth_context_defaults(&mut settings, provider); + return settings; + } if !matches!(app_type, AppType::Codex) { return live_settings; } @@ -1711,6 +1860,232 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn codex_oauth_effective_settings_backfill_gpt_context_defaults() { + let db = Database::memory().expect("create memory db"); + let mut provider = Provider::with_id( + "codex-oauth".to_string(), + "Codex".to_string(), + json!({ + "env": { + "ANTHROPIC_MODEL": "gpt-5.6" + } + }), + None, + ); + provider.meta = Some(crate::provider::ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + ..Default::default() + }); + + let effective = + build_effective_settings_with_common_config(&db, &AppType::Claude, &provider) + .expect("build effective settings"); + assert_eq!( + effective["env"]["CLAUDE_CODE_MAX_CONTEXT_TOKENS"], + json!("372000") + ); + assert_eq!( + effective["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"], + json!("372000") + ); + } + + #[test] + fn codex_oauth_context_defaults_preserve_user_overrides() { + let db = Database::memory().expect("create memory db"); + let mut provider = Provider::with_id( + "codex-oauth".to_string(), + "Codex".to_string(), + json!({ + "env": { + "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "500000", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "350000" + } + }), + None, + ); + provider.meta = Some(crate::provider::ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + ..Default::default() + }); + + let effective = + build_effective_settings_with_common_config(&db, &AppType::Claude, &provider) + .expect("build effective settings"); + assert_eq!( + effective["env"]["CLAUDE_CODE_MAX_CONTEXT_TOKENS"], + json!("500000") + ); + assert_eq!( + effective["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"], + json!("350000") + ); + } + + #[test] + fn codex_oauth_context_defaults_ignore_legacy_common_config_values() { + let db = Database::memory().expect("create memory db"); + db.set_config_snippet( + AppType::Claude.as_str(), + Some( + json!({ + "env": { + "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "262144", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "262144" + } + }) + .to_string(), + ), + ) + .expect("save legacy common config"); + let mut provider = Provider::with_id( + "codex-oauth".to_string(), + "Codex".to_string(), + json!({ "env": { "ANTHROPIC_MODEL": "gpt-5.6" } }), + None, + ); + provider.meta = Some(crate::provider::ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + common_config_enabled: Some(true), + ..Default::default() + }); + + let effective = + build_effective_settings_with_common_config(&db, &AppType::Claude, &provider) + .expect("build effective settings"); + assert_eq!( + effective["env"]["CLAUDE_CODE_MAX_CONTEXT_TOKENS"], + json!("372000") + ); + assert_eq!( + effective["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"], + json!("372000") + ); + } + + #[test] + fn codex_oauth_context_defaults_skip_non_gpt56_models() { + let db = Database::memory().expect("create memory db"); + db.set_config_snippet( + AppType::Claude.as_str(), + Some( + json!({ + "env": { + "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "262144", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "262144" + } + }) + .to_string(), + ), + ) + .expect("save legacy common config"); + let mut provider = Provider::with_id( + "codex-oauth".to_string(), + "Codex".to_string(), + json!({ + "env": { + "ANTHROPIC_MODEL": "gpt-5.5", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "300000" + } + }), + None, + ); + provider.meta = Some(crate::provider::ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + common_config_enabled: Some(true), + ..Default::default() + }); + + let effective = + build_effective_settings_with_common_config(&db, &AppType::Claude, &provider) + .expect("build effective settings"); + // 旧模型不注入 372K 默认值,遗留共享片段带进来的值也要剥掉 + assert!(effective["env"] + .get("CLAUDE_CODE_MAX_CONTEXT_TOKENS") + .is_none()); + // 用户显式写在供应商配置里的值仍然生效 + assert_eq!( + effective["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"], + json!("300000") + ); + } + + /// 往返不动点:注入产物只活在 live,切走回灌后存储配置必须与注入前一致, + /// 否则程序默认值固化成"用户显式值",之后调默认值永远压不动。 + #[test] + fn codex_oauth_backfill_strips_injected_context_defaults() { + let db = Database::memory().expect("create memory db"); + let mut provider = Provider::with_id( + "codex-oauth".to_string(), + "Codex".to_string(), + json!({ "env": { "ANTHROPIC_MODEL": "gpt-5.6" } }), + None, + ); + provider.meta = Some(crate::provider::ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + ..Default::default() + }); + + // 模拟写 live:注入了两个上下文默认值 + let live = build_effective_settings_with_common_config(&db, &AppType::Claude, &provider) + .expect("build effective settings"); + assert_eq!( + live["env"]["CLAUDE_CODE_MAX_CONTEXT_TOKENS"], + json!("372000") + ); + + // 模拟切走回灌:注入产物被剥掉,其余字段原样保留 + let backfilled = + strip_common_config_from_live_settings(&db, &AppType::Claude, &provider, live); + assert!(backfilled["env"] + .get("CLAUDE_CODE_MAX_CONTEXT_TOKENS") + .is_none()); + assert!(backfilled["env"] + .get("CLAUDE_CODE_AUTO_COMPACT_WINDOW") + .is_none()); + assert_eq!(backfilled["env"]["ANTHROPIC_MODEL"], json!("gpt-5.6")); + } + + #[test] + fn codex_oauth_backfill_keeps_user_context_values() { + let db = Database::memory().expect("create memory db"); + let mut provider = Provider::with_id( + "codex-oauth".to_string(), + "Codex".to_string(), + json!({ + "env": { + "ANTHROPIC_MODEL": "gpt-5.6", + "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "500000" + } + }), + None, + ); + provider.meta = Some(crate::provider::ProviderMeta { + provider_type: Some("codex_oauth".to_string()), + ..Default::default() + }); + + // live 里:MAX 是用户显式值;ACW 被用户手改成了非默认数字 + let live = json!({ + "env": { + "ANTHROPIC_MODEL": "gpt-5.6", + "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "500000", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "300000" + } + }); + let backfilled = + strip_common_config_from_live_settings(&db, &AppType::Claude, &provider, live); + assert_eq!( + backfilled["env"]["CLAUDE_CODE_MAX_CONTEXT_TOKENS"], + json!("500000") + ); + assert_eq!( + backfilled["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"], + json!("300000") + ); + } + /// C5 回归锁:前端表单的合并/剥离必须走 toml_edit 文档模型。 /// smol-toml 的 parse→merge→stringify 整文档重序列化会丢注释、 /// 按字母序重排键、并为 dotted 表生成多余的空父表头。 diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 9c78ad058..9dcf52948 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -722,6 +722,8 @@ mod tests { "ANTHROPIC_BASE_URL": "https://example.com", "ANTHROPIC_MODEL": "claude-x", "CLAUDE_CODE_SUBAGENT_MODEL": "gpt-5.4-mini", + "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "400000", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "400000", // 可共享、非机密配置(复数 _TOKENS 不应被误剥) "ENABLE_TOOL_SEARCH": "true", "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "8192" @@ -767,6 +769,12 @@ mod tests { assert!(env .and_then(|e| e.get("CLAUDE_CODE_SUBAGENT_MODEL")) .is_none()); + assert!(env + .and_then(|e| e.get("CLAUDE_CODE_MAX_CONTEXT_TOKENS")) + .is_none()); + assert!(env + .and_then(|e| e.get("CLAUDE_CODE_AUTO_COMPACT_WINDOW")) + .is_none()); // 可共享的非机密配置必须保留(含复数 _TOKENS 不被误剥) assert_eq!( @@ -2935,6 +2943,11 @@ impl ProviderService { "ANTHROPIC_DEFAULT_FABLE_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", "CLAUDE_CODE_SUBAGENT_MODEL", + // Context limits follow the actual upstream model. Sharing these + // across providers can cap GPT/Kimi to the wrong window and make + // Claude Code compact too early or miss the upstream limit. + "CLAUDE_CODE_MAX_CONTEXT_TOKENS", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW", "ANTHROPIC_BASE_URL", ]; diff --git a/src/config/claudeDesktopProviderPresets.ts b/src/config/claudeDesktopProviderPresets.ts index 5c58d10ed..b3490f101 100644 --- a/src/config/claudeDesktopProviderPresets.ts +++ b/src/config/claudeDesktopProviderPresets.ts @@ -394,7 +394,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [ apiFormat: "openai_responses", providerType: "codex_oauth", requiresOAuth: true, - modelRoutes: brandedRoutes("gpt-5.5", "gpt-5.5", "gpt-5.4-mini"), + modelRoutes: brandedRoutes("gpt-5.6", "gpt-5.6", "gpt-5.6-luna"), icon: "openai", iconColor: "#000000", }, diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index e688138b7..799fa6947 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -1227,10 +1227,32 @@ export const providerPresets: ProviderPreset[] = [ // base_url 由代理后端强制重写为 chatgpt.com/backend-api/codex // 用户无需配置 ANTHROPIC_BASE_URL: "https://chatgpt.com/backend-api/codex", - ANTHROPIC_MODEL: "gpt-5.5", - ANTHROPIC_DEFAULT_HAIKU_MODEL: "gpt-5.4-mini", - ANTHROPIC_DEFAULT_SONNET_MODEL: "gpt-5.5", - ANTHROPIC_DEFAULT_OPUS_MODEL: "gpt-5.5", + ANTHROPIC_MODEL: "gpt-5.6", + ANTHROPIC_DEFAULT_HAIKU_MODEL: "gpt-5.6-luna", + ANTHROPIC_DEFAULT_SONNET_MODEL: "gpt-5.6", + ANTHROPIC_DEFAULT_OPUS_MODEL: "gpt-5.6", + // Claude Code falls back to a 200K context window for unrecognized + // non-Claude model ids. The ChatGPT Codex backend catalogs gpt-5.6 + // at a 372K window with a ~353K effective budget (openai/codex#31860), + // not the 1.05M API window. Declare 372K for both knobs: Claude Code's + // built-in output reserve and compact buffer already keep the compact + // trigger below the effective budget. + CLAUDE_CODE_MAX_CONTEXT_TOKENS: "${CLAUDE_CODE_MAX_CONTEXT_TOKENS}", + CLAUDE_CODE_AUTO_COMPACT_WINDOW: "${CLAUDE_CODE_AUTO_COMPACT_WINDOW}", + }, + }, + templateValues: { + CLAUDE_CODE_MAX_CONTEXT_TOKENS: { + label: "Max Context Tokens", + placeholder: "372000", + defaultValue: "372000", + editorValue: "372000", + }, + CLAUDE_CODE_AUTO_COMPACT_WINDOW: { + label: "Auto Compact Window", + placeholder: "372000", + defaultValue: "372000", + editorValue: "372000", }, }, category: "third_party", diff --git a/src/config/codexTemplates.ts b/src/config/codexTemplates.ts index 42bf0c962..989d4b41d 100644 --- a/src/config/codexTemplates.ts +++ b/src/config/codexTemplates.ts @@ -14,7 +14,7 @@ export interface CodexTemplate { */ export function getCodexCustomTemplate(): CodexTemplate { const config = `model_provider = "custom" -model = "gpt-5.5" +model = "gpt-5.6" model_reasoning_effort = "high" disable_response_storage = true diff --git a/tests/config/claudeProviderPresets.test.ts b/tests/config/claudeProviderPresets.test.ts index beb031ea2..d0b31e683 100644 --- a/tests/config/claudeProviderPresets.test.ts +++ b/tests/config/claudeProviderPresets.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { providerPresets } from "@/config/claudeProviderPresets"; +import { applyTemplateValues } from "@/utils/providerConfigUtils"; describe("Kimi For Coding Provider Preset", () => { const kimiForCoding = providerPresets.find( @@ -28,6 +29,47 @@ describe("Kimi For Coding Provider Preset", () => { }); }); +describe("Codex Provider Preset", () => { + const codex = providerPresets.find((p) => p.name === "Codex"); + + it("should include the Codex preset", () => { + expect(codex).toBeDefined(); + }); + + it("should override Claude Code's 200K fallback for GPT models", () => { + const env = (codex!.settingsConfig as any).env; + expect(env).toHaveProperty( + "CLAUDE_CODE_MAX_CONTEXT_TOKENS", + "${CLAUDE_CODE_MAX_CONTEXT_TOKENS}", + ); + expect(env).toHaveProperty( + "CLAUDE_CODE_AUTO_COMPACT_WINDOW", + "${CLAUDE_CODE_AUTO_COMPACT_WINDOW}", + ); + }); + + it("should expose the Codex-catalog 372K window for both context knobs", () => { + const values = codex!.templateValues as any; + expect(values?.CLAUDE_CODE_MAX_CONTEXT_TOKENS).toMatchObject({ + defaultValue: "372000", + editorValue: "372000", + }); + expect(values?.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toMatchObject({ + defaultValue: "372000", + editorValue: "372000", + }); + }); + + it("should resolve both context placeholders into Claude Code env values", () => { + const config = applyTemplateValues( + codex!.settingsConfig, + codex!.templateValues, + ) as any; + expect(config.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS).toBe("372000"); + expect(config.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe("372000"); + }); +}); + describe("AWS Bedrock Provider Presets", () => { const bedrockAksk = providerPresets.find( (p) => p.name === "AWS Bedrock (AKSK)",