feat(codex): add unified session history toggle for official providers

Codex buckets resume history by the model_provider id recorded in each
session: official runs (no key, built-in "openai") and cc-switch
third-party runs (shared "custom") are mutually invisible in the resume
picker. Add an opt-in setting that runs official providers under the
shared "custom" id so future official sessions land in the same history
bucket as third-party ones. Forward-only by design: existing sessions
are not migrated.

When enabled, official live config.toml gets model_provider = "custom"
plus a [model_providers.custom] entry that mirrors the built-in openai
provider (requires_openai_auth routes auth to the ChatGPT login in
auth.json, name "OpenAI" keeps is_openai() feature gates, explicit
supports_websockets/wire_api restore built-in defaults). auth.json is
untouched.

Key invariants:
- Injection lives only in the live config: switch-away backfill strips
  the exact injected shape, so stored provider configs stay clean and
  turning the toggle off fully reverts on the next write.
- Toggle changes apply immediately via a takeover-aware reapply: when
  the proxy owns the live config (backup/placeholder present), only the
  live backup is updated, mirroring the provider-switch path.
- The takeover backup path runs the same injection so a takeover
  release restores a config that still carries the unified routing.
- Injection refuses to activate a foreign [model_providers.custom]
  table (e.g. stale entry with a third-party base_url) to avoid routing
  ChatGPT OAuth traffic to an unknown backend.

The toggle lives under Settings → Codex App Enhancements; the
description warns that resuming old sessions across providers may fail
because encrypted_content reasoning only decrypts on the backend that
created it (upstream treats cross-provider resume as unsupported).
This commit is contained in:
Jason
2026-06-12 10:40:51 +08:00
parent 4f355970e1
commit 948d762792
14 changed files with 406 additions and 2 deletions
+13
View File
@@ -596,6 +596,19 @@ fn restore_live_settings_for_provider_backfill(
);
}
// 统一会话开关注入的共享 `custom` 路由只属于 live 配置;切换回填时
// 必须剥掉,否则官方供应商的存储配置被污染,关闭开关后无法还原。
if provider.category.as_deref() == Some("official") {
if let Err(err) =
crate::codex_config::strip_codex_unified_session_bucket_from_settings(&mut settings)
{
log::warn!(
"Failed to strip unified session bucket while backfilling '{}': {err}",
provider.id
);
}
}
// `modelCatalog` is a cc-switchprivate field whose SSOT is the DB. Live's
// `config.toml` only carries a lossy projection (`model_catalog_json` →
// generated catalog file) that proxy takeover/restore cycles and Codex.app
+42
View File
@@ -42,6 +42,48 @@ use live::{
};
use usage::validate_usage_script;
/// 统一会话开关变更后,立即按新开关状态重写当前官方 Codex 供应商的
/// live 配置,使开关即时生效(无需等下一次切换)。
/// 当前供应商非官方(或不存在)时为 no-op:注入只作用于官方配置,
/// 第三方 live 配置不受开关影响。
pub fn reapply_current_codex_official_live(state: &AppState) -> Result<bool, AppError> {
let current_id = ProviderService::current(state, AppType::Codex)?;
if current_id.is_empty() {
return Ok(false);
}
let providers = state.db.get_all_providers(AppType::Codex.as_str())?;
let Some(provider) = providers.get(&current_id) else {
return Ok(false);
};
if provider.category.as_deref() != Some("official") {
return Ok(false);
}
// 代理接管期间 live 归代理所有(开启代理时官方供应商只警告不拦截,
// 二者可以共存)。与切换/保存路径一致:以 backup/占位符为所有权信号,
// 只更新备份,注入后的配置由接管释放时的恢复路径落盘。
let has_live_backup =
futures::executor::block_on(state.db.get_live_backup(AppType::Codex.as_str()))
.ok()
.flatten()
.is_some();
let live_taken_over = state
.proxy_service
.detect_takeover_in_live_config_for_app(&AppType::Codex);
if has_live_backup || live_taken_over {
futures::executor::block_on(
state
.proxy_service
.update_live_backup_from_provider(AppType::Codex.as_str(), provider),
)
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
return Ok(true);
}
live::write_live_with_common_config(&state.db, &AppType::Codex, provider)?;
Ok(true)
}
/// Provider business logic service
pub struct ProviderService;
+8
View File
@@ -2033,6 +2033,14 @@ impl ProxyService {
)?;
Self::preserve_codex_oauth_auth_in_backup(&mut effective_settings, existing_value)?;
}
// 统一会话开关:备份是接管释放时恢复 live 的来源,官方配置的
// 共享 custom 路由注入必须落在备份里,否则恢复后开关失效。
crate::codex_config::apply_codex_unified_session_bucket_to_settings(
provider.category.as_deref(),
&mut effective_settings,
)
.map_err(|e| format!("注入统一会话路由失败: {e}"))?;
}
let backup_json = match app_type_enum {