随笔

拆解 cc-switch 对 Codex & Claude Code 支持,安全引入第三方模型

从 Codex 的 auth/config 分离、Claude Code 的 settings.json 接管、cc-switch 本地代理与三级恢复机制,拆解第三方模型接入的安全边界。

2026-06-26 CodexClaude Codecc-switch第三方模型安全

这次我不是单纯想把 Codex 或 Claude Code 接到第三方模型上,而是想保留 OpenAI 账号登录态,继续使用 Plus / Pro 额度;在额度紧张或模型能力需要补位时,引入 DeepSeek、Kimi、GLM、OpenRouter 这类第三方 API;并且让第三方模型作为替补,在原 OpenAI 模型运作的 agent session 中继续服务,同时不触发 OpenAI 账号重新登录授权。

这个目标看起来像是增加切换模型的能力,实际落到源码实现上必须关注如下几个方面:

  • auth.json 能不能不被第三方 API key 覆盖。
  • config.tomlsettings.json 会被怎样改写。
  • Codex 的历史 session 是按哪个 model_provider_id 分桶。
  • 本地代理如何把 Responses、Chat Completions、Anthropic Messages、Gemini 这些协议接起来。
  • 程序退出、崩溃或切换失败时,live 配置能不能恢复。

分析 Codex 和 cc-switch 源码后,结论很明确:cc-switch 可以满足“不触发 OpenAI 重新登录”,但前提是开启正确开关;“原 session 继续服务”也可以做,但依赖统一会话桶和存量迁移,不是零副作用。Claude Code 则没有 Codex 这种 auth/config 分离,安全重点要换成备份和占位符恢复。

短版操作总结:先选目标,再选路线

如果目标只是“保留 OpenAI 登录态,同时让 Codex 临时走第三方模型”,操作顺序要放在切换之前:

  1. 先确认 ~/.codex/auth.json 里已有正常 OpenAI / ChatGPT 登录态。
  2. 在 cc-switch 的 Codex 应用增强设置里,先打开“切换第三方时保留官方登录”,对应配置是 preserve_codex_official_auth_on_switch = true
  3. 再添加或启用第三方 Codex provider,确认它不是 official category,并填好 base URL、API key 和模型映射。
  4. 如果第三方只提供 Chat Completions,就必须让 Codex 流量走 cc-switch 本地代理,由代理把 Responses 请求转成上游协议。
  5. 切换后检查 auth.json 没有被改写;如果 config.toml 出现 experimental_bearer_token,把它当密钥文件管理。

如果目标是“让第三方模型接着原 OpenAI session 服务”,只保护 auth.json 还不够:

  1. 仍然先打开 preserve_codex_official_auth_on_switch
  2. 再打开 unify_codex_session_history,让官方和第三方新会话使用同一个 custom 会话桶。
  3. 如果要迁移旧的官方 session,同时勾选“迁入既有官方会话”,也就是让 unify_codex_migrate_existing 生效。
  4. 迁移前确认 live config.toml 已经实际注入 model_provider = "custom";源码里迁移会检查这一点,不满足就跳过。
  5. 接受迁移会原位改写 session jsonl 和 state_*.sqlite。恢复前要先关闭统一会话开关,再走 cc-switch 的备份还原。

如果更看重隔离而不是无缝续接,就用 Profile V2 + cc-switch proxy:默认 config.tomlauth.json 不动,单独创建 <name>.config.toml,用 codex --profile <name> 启动。代价是会话列表天然分桶,不适合把桌面版里的原 OpenAI session 当成同一个列表继续使用。

先说 Codex:安全边界来自文件分层

Codex 的核心配置分成两个文件:

~/.codex/
├── auth.json      # 身份认证层:ChatGPT OAuth、OpenAI API key、Agent Identity 等
└── config.toml    # 模型路由层:model_provider、base_url、wire_api、bearer token 等

Codex 的 provider 认证解析会先看当前 provider 自己有没有 env_keyexperimental_bearer_token。如果有,就直接构造 Bearer 认证;只有 provider 自己没有 token 时,才回退到 auth.json 里的官方登录材料。

这意味着第三方 provider 的 API key 可以放在 provider 配置里,而不必写进 auth.json。于是 auth.json 继续代表“以哪个 OpenAI / ChatGPT 账号登录”,config.toml 负责“这次模型请求发往哪里”。

安全引入第三方模型,第一层就是守住这条分界线。

cc-switch 当前方案:Live Rewrite,不是 Profile 隔离

cc-switch v3.16.x 这一路径,本质上是 live rewrite:它会原地改写 ~/.codex/config.toml,必要时还会改写 ~/.codex/auth.json

先打开这个关键开关:

preserve_codex_official_auth_on_switch = true

操作上不能等切换后再补救。正确顺序是:先在 cc-switch 设置页的 Codex 增强项里打开“切换第三方时保留官方登录”,再添加或启用第三方 Codex provider。切换前至少备份一次:

cp ~/.codex/auth.json ~/.codex/auth.json.before-cc-switch
cp ~/.codex/config.toml ~/.codex/config.toml.before-cc-switch

开启后,第三方 provider 切换时,cc-switch 的写入分发逻辑会让 should_write_auth = false,于是只写 config.toml,不写 auth.json。第三方 API key 会从 provider 存储里的 auth.OPENAI_API_KEY 被提取出来,再写入 config.tomlexperimental_bearer_token

也就是说,在这个安全路径下:

  • auth.json 保留 OpenAI / ChatGPT 登录态。
  • config.toml 被改成第三方 provider 路由。
  • 第三方 API key 进入 experimental_bearer_token
  • Codex 运行时优先使用 provider token,不回退到 auth.json

这可以满足“不触发 OpenAI 重新登录授权”。但它不是架构级绝对安全,因为 cc-switch 的默认开关是关闭的;如果没有先打开这个开关,切到第三方 provider 就可能覆盖 auth.json,把官方 OAuth 登录态变成 API key 模式。

所以,可以给这样一个结论:cc-switch 能保护 Codex 官方登录态,但这是一个配置正确后的结果,不是默认行为。

API key:env_key 更干净,experimental_bearer_token 是 cc-switch 的落盘路径

Codex 原生 provider 配置里,env_keyexperimental_bearer_token 都可以提供 Bearer token。源码注释也把话说得很清楚:experimental_bearer_token 可用于程序化场景,但出于安全原因更推荐 env_key

两者差异很直接:

[model_providers.deepseek]
name = "DeepSeek"
base_url = "https://api.deepseek.com/v1"
env_key = "DEEPSEEK_API_KEY"
wire_api = "responses"

这种方式下,API key 在环境变量里,config.toml 不直接保存密钥。

而 cc-switch 当前 live rewrite 路径通常会变成:

model_provider = "custom"
model = "deepseek-chat"

[model_providers.custom]
name = "DeepSeek"
base_url = "https://api.deepseek.com"
wire_api = "responses"
experimental_bearer_token = "sk-..."
model_catalog_json = "cc-switch-model-catalog.json"

这里的风险不是“因为使用了 cc-switch,所以必须额外恢复”。真正的因果关系是:cc-switch 把第三方 API key 写入 experimental_bearer_token,因此 config.toml 本身包含敏感 token,文件权限和备份管理就必须按密钥文件对待。

如果配置文件里有 token,更合理的是收紧权限,例如只允许当前用户读写:

chmod 600 ~/.codex/config.toml

如果能接受额外的环境变量管理,env_key 仍然是更干净的方式。

本地代理:cc-switch 的真正价值在协议转换

只改 config.toml 还不够。很多第三方模型并不提供 Codex 期望的 Responses API。DeepSeek 等供应商常见的是 Chat Completions;Claude 侧是 Anthropic Messages;Gemini 又是另一套格式。

cc-switch 的本地代理负责把这些协议接起来。Codex 仍然向本地代理发 Responses 请求,代理再根据当前 provider 做模型映射、认证注入、请求体转换和流式响应转换。

以 Codex 走 DeepSeek Chat 为例,链路大致是:

Codex Responses 请求
  -> cc-switch 本地代理
  -> 根据 DB 当前 provider 选择 DeepSeek
  -> model_mapper 做客户端模型名到上游模型名映射
  -> transform_codex_chat 把 Responses 请求转成 Chat Completions
  -> 注入 DeepSeek API key
  -> 转发到上游 /chat/completions
  -> streaming_codex_chat 把 Chat SSE 转回 Responses SSE
  -> Codex 收到标准 Responses 流

这里不是透明转发。代理还要处理工具调用的双向映射,把 Codex 的 namespace tool / custom tool 压平成 Chat function tool,再把上游返回的 tool call 恢复成 Codex 能识别的输出项。它还要处理 DeepSeek 一类模型可能输出的 <think>...</think> 推理块,把推理内容和普通文本拆开;usage 统计也要尽量归到上游真实模型,而不是客户端别名。

所以 cc-switch 相比 Cloudflare AI Gateway 这类 Responses-only 配置方案,优势在代理层:它能承接 Chat Completions、Anthropic、Gemini 等非 Responses 供应商。代价是本地代理和配置接管带来更多恢复责任。

“原 OpenAI session 继续服务”不是自然发生

Codex 的历史会话不是只按文件存在与否展示,它还按 model_provider_id 分桶。

源码里,session meta 会记录 model_provider;本地 resume picker 在非远程 workspace 下,会用当前 config.model_provider_id 过滤历史。于是:

  • 官方 OpenAI 默认 session 在 openai 桶。
  • cc-switch 第三方 session 通常在 custom 桶。
  • 切到第三方后,openai 桶里的旧 session 仍在磁盘上,但 resume 列表默认看不到。

cc-switch 解决这个问题的方式是 unify_codex_session_history。当它开启,并且切回官方 provider 时,cc-switch 会尝试把官方配置注入成共享的 custom 路由:

model_provider = "custom"

[model_providers.custom]
name = "OpenAI"
requires_openai_auth = true
supports_websockets = true
wire_api = "responses"

注意这里的 requires_openai_auth = true:官方 provider 仍然走 auth.json 的 ChatGPT 登录材料,只是 model_provider_id 也变成 custom。这样官方和第三方新建的 session 才能进入同一个 resume 桶。

但存量 session 还要另说。已经存在的 openai 桶 session,不会因为打开开关自动变成 custom。cc-switch 还有“迁入既有官方会话”的机制,会原位改写 session jsonl 和 state_*.sqlite 里的 provider 字段,并在 cc-switch 数据目录下做备份。

实际操作可以压缩成四步:

  1. 先打开 preserve_codex_official_auth_on_switch,确保第三方切换不会写 auth.json
  2. 打开 unify_codex_session_history;如果要迁移旧 session,同时勾选“迁入既有官方会话”,让 unify_codex_migrate_existing 进入待执行状态。
  3. 切回或启用 OpenAI Official,让 cc-switch 在 live config.toml 里实际注入共享 custom provider。源码里迁移前会检查 live 配置是否真的路由到 custom,如果没有满足条件,会以 live_not_unified 之类的状态跳过。
  4. 再切到第三方 provider,并用 Codex 的 resume 列表确认同一个 custom 桶里能看到需要继续的会话。

恢复也有前置条件:如果要从 cc-switch 备份里还原迁移过的官方历史,必须先关闭 unify_codex_session_history。源码里的还原函数会拒绝在统一会话开关仍然开启时恢复,避免一边统一、一边回滚 provider 字段。

所以需求“第三方模型可以在原 OpenAI 模型运作的 agent session 中继续服务”,源码层面的结论是:

  • 只开启保护 auth 的开关,不够。
  • 还要开启统一会话历史。
  • 如果是存量官方 session,还要执行迁移。
  • 迁移不是复制,而是原位改写,有备份和还原逻辑,但还原依赖 cc-switch 的备份账本。
  • 即使 session 能 resume,模型能力也可能变化,tool calling、reasoning、上下文窗口都可能带来行为差异。

这是一种“有条件满足”,不能写成无条件平滑接力。

Profile V2:适合隔离,不适合无缝续接原 session

Profile V2 这条路线值得单独看,因为它把配置隔离放在独立 profile 文件里。Codex 源码支持 --profile <name>,会读取:

~/.codex/<name>.config.toml

profile 名称只能由 ASCII 字母、数字、下划线、连字符组成,路径解析固定落在 Codex home 下。这意味着可以把第三方 provider 写进独立 profile 文件,而不是改写默认 ~/.codex/config.toml

Cloudflare AI Gateway 的官方示例就是这个方向:

model_provider = "cloudflare-ai-gateway"
model = "gpt-5.5"

[model_providers.cloudflare-ai-gateway]
name = "Cloudflare AI Gateway"
base_url = "https://gateway.ai.cloudflare.com/v1/<ACCOUNT_ID>/<GATEWAY_ID>/openai"
env_key = "CLOUDFLARE_API_KEY"
wire_api = "responses"

这个方案的优点很清楚:

  • auth.json 不动。
  • 默认 config.toml 不动。
  • API key 通过 env_key 提供,不落盘。
  • 在 CLI 中,不带 --profile 启动就是官方默认配置。

但它不能直接回答“Codex 桌面版里让原 OpenAI session 无缝继续服务”这个问题。从 Codex 的 resume picker、codex --resume 最近会话、App Server / IDE session 列表、state DB 查询和 jsonl 扫描实现看,这些入口都会按当前 model_provider_id 过滤。Profile V2 文件如果写的是 model_provider = "deepseek-via-cc",它创建和显示的就是 deepseek-via-cc 桶;默认 OpenAI 仍在 openai 桶。

所以 Profile V2 更适合“隔离使用不同供应商”,不适合直接实现“原 OpenAI session 在桌面版里继续显示并接着服务”。如果手里有 thread id,codex --resume <thread-id> 这种精确恢复可以跨桶找到会话;但日常列表、最近会话和桌面/IDE 侧的会话列表仍会体现分桶差异。

更稳妥的混合方向是:Profile V2 负责文件隔离,cc-switch 本地代理负责协议转换,同时接受 session 隔离这个事实。 profile 文件把 base_url 指向本地代理,API key 尽量用环境变量;代理再根据 cc-switch DB 当前 provider 做上游转发和格式转换。这样默认 config.tomlauth.json 都不用改,但如果目标是和官方 session 混用,仍然要回到统一 model_provider_id、存量迁移和模型兼容性这些问题。

Claude Code:没有 auth/config 分离,风险模型不同

Claude Code 和 Codex 最大的差异,是它没有 auth.json / config.toml 的职责分离。Claude Code 的核心配置在:

~/.claude/settings.json

如果新版本不存在这个文件,cc-switch 源码里会兼容旧的 ~/.claude/claude.json;默认写入目标仍是 settings.json

这个 JSON 里同时包含 base URL、token、模型别名和其他设置。例如:

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://api.anthropic.com",
    "ANTHROPIC_AUTH_TOKEN": "sk-ant-...",
    "ANTHROPIC_MODEL": "claude-sonnet-..."
  },
  "includeCoAuthoredBy": false
}

因此 Claude Code 没有 Codex 那种“只改路由,不动身份文件”的优雅路径。普通供应商切换时,cc-switch 会清理内部字段后直接写 live settings.json。代理接管时,它必须把 ANTHROPIC_BASE_URL 改成本地代理地址,并把可能存在的 token key 替换成 PROXY_MANAGED

Claude Code 侧可能被处理的 token key 包括:

  • ANTHROPIC_AUTH_TOKEN
  • ANTHROPIC_API_KEY
  • OPENROUTER_API_KEY
  • OPENAI_API_KEY

这不是为了隐藏风险,而是为了在单文件配置下留下清晰接管标记:只要看到本地代理 base URL 和 PROXY_MANAGED,就知道 live 配置处于代理状态。真正的安全底线变成:退出或崩溃后必须恢复原配置,至少要清理占位符和本地代理地址。

所以 Claude Code 的关键词不是“保护 auth.json”,而是“完整备份、占位符检测、三级恢复”。

Common Config 要看具体写入哪个文件

cc-switch 的 common config 是跨供应商共享配置。

在 cc-switch 的存储层,common config 存在 DB 的 settings 表里,key 类似:

common_config_codex
common_config_claude
common_config_gemini

写入 live 时:

  • Claude Code 的 common config 是 JSON,合并到 ~/.claude/settings.json 的顶层对象,常见影响是 env 字段。
  • Codex 的 common config 是 TOML,合并到 ~/.codex/config.tomlconfig 文本里,例如 [model_providers.custom] 或共享表段。
  • Gemini 的 common config 是 JSON,作用到 .env 风格的环境变量映射。

反向 backfill 时,cc-switch 还会把 common config 从 live 配置中剥离。原因很简单:DB 中存储的 provider 应该是供应商自己的配置,不应该混入“供应商配置 + 公共配置 + 运行时临时字段”。如果不剥离,切换次数多了以后,配置差异会越来越难解释。

更准确地说:write_live_with_common_configbuild_effective_settings_with_common_configstrip_common_config_from_live_settings 的实现看,common config 是写 live 前合并、读回 DB 前剥离的。

安全引入第三方模型的操作底线

按当前 cc-switch 能力来做 Codex,操作底线要收窄:

  • 先确认 auth.json 已经有正常 OpenAI / ChatGPT 登录态。
  • 在 cc-switch 里先打开 preserve_codex_official_auth_on_switch,再切第三方。
  • 确认第三方 provider 的 category 不是 official
  • 如果要在原 OpenAI session 中继续服务,再评估是否开启 unify_codex_session_history 和存量迁移。
  • 接受存量迁移是原位改写,先确认备份位置和还原条件。
  • 如果第三方只支持 Chat Completions,必须走本地代理做协议转换。
  • 如果 config.toml 里出现 experimental_bearer_token,按密钥文件管理权限。

另一条更保守的路线,是 Profile V2 + cc-switch proxy 的混合路径:

  • 默认 config.toml 不动。
  • auth.json 不动。
  • 每个第三方生成独立 <name>.config.toml
  • profile 里尽量使用 env_key
  • profile 的 base_url 指向 cc-switch 本地代理。
  • 代理继续负责第三方协议转换。

这条路线把文件层风险降得更低,但会牺牲一部分“默认切换即生效”的便利,并且默认带来 session 隔离。它适合把不同供应商当成不同工作空间使用;如果要让原 OpenAI session 在桌面版里继续出现在列表中,仍然要用统一会话桶或迁移方案。

Claude Code 要换一套检查标准:

  • 切换前确认 settings.json 或旧版 claude.json 的备份存在。
  • 接管时接受 ANTHROPIC_BASE_URL 和 token key 被写成代理占位符。
  • 退出后检查 PROXY_MANAGED 和本地代理地址是否消失。
  • 不把 Claude Code 的供应商切换理解成 Codex 式“身份层和路由层分离”。

结论:安全引入第三方模型,要分清三种能力

cc-switch 的能力可以拆成三层看。

第一层是 auth 保护。Codex 侧可以通过 preserve_codex_official_auth_on_switch 保留 OpenAI 登录态,避免第三方 API key 覆盖 auth.json。这是“不触发重新登录”的基础。

第二层是 session 连续性。这依赖 model_provider_id 分桶处理。想让第三方模型接着原 OpenAI agent session 服务,需要统一会话桶;已有 session 还需要迁移。这是可做的,但不是无副作用。

第三层是 协议转换。这是 cc-switch 本地代理的核心价值。没有这层,很多 Chat Completions 或 Anthropic/Gemini 风格供应商不能直接接进 Codex。

Claude Code 则是另一套风险模型:它没有 Codex 的 auth/config 分离,所以安全重点不在“不写 auth.json”,而在接管前备份、接管中占位符、接管后恢复。

所以“安全引入第三方模型”不是让工具无感替换模型,而是做到四件事:官方登录态不被误伤,真实流量去向可解释,session 连续性的代价说清楚,异常退出后配置能恢复。

能同时满足这些条件,第三方模型才算被安全地接入,而不是临时把请求转了出去。