mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
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
This commit is contained in:
@@ -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<String, Value>>) -> 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 表生成多余的空父表头。
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user