ZT-002AI CodingTESTED

Codex 如何切换 OpenAI Compatible API

切换 Provider 不只是改一个地址。地址、模型、鉴权方式和接口能力必须同时对上,才算真正切换成功。

  • Codex
  • OpenAI Compatible API
  • API Provider
Codex 多 Provider 切换封面图,展示 codex-switch、Official、PackyApi 与 DevPoolAi

最终你会得到什么

完成本文后,你会保留独立的 Official 配置,并能用单独的 Profile 接入 OpenAI Compatible API;切换、验证和恢复都通过 codex-switch 完成,不会覆盖官方登录状态。

主流程只有 5 步:

  1. 配置 Provider
  2. 创建独立 Profile
  3. 安装 codex-switch
  4. 切换并验证
  5. 恢复 Official

先说结论

如果你只是偶尔测试一个中转服务,最稳的做法是保留官方配置,再为第三方准备单独的 CODEX_HOME 目录。这样切换只影响当前命令,失败时关掉这个终端即可回滚,不需要覆盖主目录里的登录状态。

OpenAI Compatible 只表示服务提供了相近的 HTTP 接口,不代表 Codex 的所有能力都兼容。先确认目标服务支持 Codex 当前使用的 API 形态、模型名和响应格式,再开始配置。

Codex 到底读取哪些配置

Codex 的用户级配置位于 ~/.codex/,其中包括 config.toml、auth.json 和 skills/。本文使用一个放在 $HOME/bin/codex-switch 的自建脚本,以及 $HOME/.codex-profiles/ Profile 目录。这个脚本不是 Codex 官方内置命令。

下面的目录树只使用 $HOME,不展示真实用户名、路径或凭据,用来说明官方配置、第三方 Profile 和共享 Skills 的层级边界;文件内容仍以你的本机配置为准。

通常可以按下面的边界理解:

文件或目录 作用 切换 Provider 时是否应复制
config.toml 模型、Provider、接口和本地行为配置 按方案复制或单独维护
auth.json 官方登录或其他鉴权状态,具体取决于版本和 Provider 不要复制或公开 Token
skills/ 用户级 Skills 通常可以共用
项目内 AGENTS.md、项目级 Skills 当前仓库的规则与工作流 与 Provider 无关,继续共用

第 2 步:创建独立 Profile

把官方订阅和第三方 Provider 分开,目录可以这样组织:

$HOME/.codex/
├── config.toml
├── auth.json
└── skills -> $HOME/.codex-shared/skills

$HOME/.codex-profiles/
├── packyapi/
│   ├── config.toml
│   └── skills -> $HOME/.codex-shared/skills
└── devpoolai/
    ├── config.toml
    └── skills -> $HOME/.codex-shared/skills

$HOME/.codex 保留官方订阅和 auth.json;第三方 Profile 只放自己的 config.toml,不复制官方 auth.json。本机脚本会拒绝使用含有 auth.json 的第三方 Profile,也会拒绝没有指向共享 Skills 目录的 skills 链接。

第 1 步:配置 Provider

先对齐四个参数:

参数 示例 检查重点
base_url https://your-provider.example/v1 是否为服务要求的根地址,是否重复写了 /v1
api_key YOUR_API_KEY 只通过环境变量或安全存储注入,不写进 Git
model provider-model-name 必须是该服务实际开放的模型 ID
wire_api / API 类型 responses 或版本要求的值 目标服务是否支持 Codex 所需的请求和响应格式

base_url 是请求拼接的根地址,不一定等于服务商官网地址;model 是服务端识别的精确字符串,不能凭产品宣传名猜。若服务只实现 Chat Completions,而当前 Codex 配置使用 Responses,改完地址仍可能失败。

下面是一份完整的 Profile config.toml 模板,字段名以你安装的 Codex 版本文档和 schema 为准:

model = "provider-model-name"
model_provider = "packyapi"
model_reasoning_effort = "medium"
disable_response_storage = true
service_tier = "default"

[model_providers.packyapi]
name = "PackyApi"
base_url = "https://your-provider.example/v1"
env_key = "PACKY_API_KEY"
wire_api = "responses"
requires_openai_auth = false

API Key 不放进这个示例文件。优先使用该版本支持的环境变量或系统密钥存储;变量名也必须以 Provider 文档为准。不要把 auth.json 当成第三方 API Key 的通用容器。

Codex OpenAI Compatible Provider 的脱敏 config.toml 配置
真实配置中的 Provider、模型、地址、接口类型与鉴权开关。

截图展示了真实配置中的 model_provider、model、model_providers、base_url、wire_api、requires_openai_auth 和已脱敏的 experimental_bearer_token 字段。未显示的密钥值和其他 Provider 仍需按各自文档配置。

正文示例使用 https://your-provider.example/v1 占位符;实际 Provider 地址请以服务文档为准。

为什么需要 codex-switch

codex-switch 是一个由用户自己创建的本地 shell 脚本,不是 Codex 官方命令。它把重复的 Profile 切换动作集中起来:列出当前 Profile、检查目录安全条件、写入 Provider 配置、设置 CODEX_HOME,以及恢复官方配置。

需要它的原因很实际:手改 config.toml 容易改错,直接覆盖 $HOME/.codex 会把第三方配置和官方 auth.json 混在一起,而多个终端或桌面进程还可能继续使用旧环境。脚本把每个 Provider 隔离到自己的目录,让官方登录状态保持独立,Skills 通过符号链接共用。

脚本使用 macOS 的 launchctl setenv/unsetenv,因此切换后要重启 Codex 或 VS Code 才会读取新的 CODEX_HOME。如果你使用的是纯终端并且不需要 GUI 进程,可以把同样的逻辑改成当前 shell 的 export;不要把两种行为混写。

base_url 可以由脚本写入 Profile 配置,Key 只以 env_key 指向环境变量,脚本不会把真实 Key 写进文件。model_provider 必须和表段名一致;wire_api 则必须符合你的 Codex 版本和目标服务支持的 API 形态。devpoolai 使用同样的结构,只需替换 Provider 名称、地址、模型和环境变量名。

第 3 步:安装 codex-switch

先创建目录和文件:

mkdir -p "$HOME/.local/bin"
${EDITOR:-nano} "$HOME/.local/bin/codex-switch"

把下面的核心片段用于快速理解;完整脚本可展开复制。

current_home() { launchctl getenv CODEX_HOME 2>/dev/null || true; }

switch_profile() {
  local id="${1:l}" dir="$profiles_root/${1:l}"
  [[ "$id" == official ]] && launchctl unsetenv CODEX_HOME && return
  check_profile_safety "$id"
  write_managed_config "$id"
  launchctl setenv CODEX_HOME "$dir"
}
查看完整 codex-switch 脚本

完整 codex-switch 脚本

下面的版本保留了本机脚本的核心逻辑:官方 Profile、Profile 检测、launchctl 环境变量、环境变量校验、受管理配置生成、Skills 链接检查和启动后重启提示。它没有任何真实用户名、Key 或私有 endpoint。

#!/bin/zsh
setopt errexit pipefail

typeset -r official_home="$HOME/.codex"
typeset -r profiles_root="$HOME/.codex-profiles"
typeset -r shared_skills="$HOME/.codex-shared/skills"

typeset -A profile_labels profile_base_envs profile_key_envs
typeset -A profile_models profile_efforts
profile_labels[packyapi]="PackyApi"
profile_labels[devpoolai]="DevPoolAi"
profile_base_envs[packyapi]="PACKY_BASE_URL"
profile_base_envs[devpoolai]="DEVPOOL_BASE_URL"
profile_key_envs[packyapi]="PACKY_API_KEY"
profile_key_envs[devpoolai]="DEVPOOL_API_KEY"
profile_models[packyapi]="provider-model-name"
profile_models[devpoolai]="provider-model-name"
profile_efforts[packyapi]="medium"
profile_efforts[devpoolai]="medium"

usage() { print "Usage: codex-switch [current|list|official|packyapi|devpoolai]"; }

launchd_env_value() {
  local name="$1" value=""
  case "$name" in
    PACKY_BASE_URL) value="${PACKY_BASE_URL-}" ;;
    PACKY_API_KEY) value="${PACKY_API_KEY-}" ;;
    DEVPOOL_BASE_URL) value="${DEVPOOL_BASE_URL-}" ;;
    DEVPOOL_API_KEY) value="${DEVPOOL_API_KEY-}" ;;
  esac
  [[ -n "$value" ]] && print -r -- "$value" || launchctl getenv "$name" 2>/dev/null || true
}

profile_label() { print -r -- "${profile_labels[$1]-$1}"; }
valid_profile_id() { [[ "$1" =~ '^[a-z0-9][a-z0-9_-]*$' ]]; }
current_home() { launchctl getenv CODEX_HOME 2>/dev/null || true; }

current_description() {
  local active="$(current_home)"
  [[ -z "$active" || "$active" == "$official_home" ]] && print "Official ($official_home)" && return
  if [[ "$active" == "$profiles_root"/* ]]; then
    local id="${active#$profiles_root/}"
    [[ "$id" != */* && -n "${profile_labels[$id]-}" ]] && print "$(profile_label "$id") ($active)" && return
  fi
  print "Custom ($active)"
}

profile_ids() {
  local path id
  for id in packyapi devpoolai; do [[ -d "$profiles_root/$id" ]] && print "$id"; done
  for path in "$profiles_root"/*(N/); do
    id="${path:t}"; [[ "$id" == packyapi || "$id" == devpoolai ]] && continue
    print "$id"
  done
}

check_profile_safety() {
  local id="$1" dir="$profiles_root/$1"
  [[ -d "$dir" ]] || { print -u2 "Profile directory does not exist: $dir"; return 1; }
  [[ ! -e "$dir/auth.json" && ! -L "$dir/auth.json" ]] || { print -u2 "Refusing to use $dir because auth.json exists."; return 1; }
  [[ -L "$dir/skills" && "$(readlink "$dir/skills")" == "$shared_skills" ]] || {
    print -u2 "Refusing to use $dir because skills is not linked to $shared_skills."; return 1;
  }
}

write_managed_config() {
  local id="$1" dir="$profiles_root/$1" path="$profiles_root/$1/config.toml"
  local base_env="${profile_base_envs[$id]-}" key_env="${profile_key_envs[$id]-}"
  local base_url="$(launchd_env_value "$base_env")" api_key="$(launchd_env_value "$key_env")"
  [[ -n "$base_url" && -n "$api_key" ]] || { print -u2 "Set $base_env and $key_env first."; return 1; }
  [[ "$base_url" == http://* || "$base_url" == https://* ]] || { print -u2 "$base_env must be an HTTP(S) URL."; return 1; }
  if [[ -e "$path" || -L "$path" ]] && ! grep -q '^# Managed by codex-switch$' "$path"; then
    print -u2 "Refusing to overwrite unmanaged $path"; return 1
  fi
  local temp="$(mktemp "$dir/.config.toml.XXXXXX")"; chmod 600 "$temp"
  cat > "$temp" <<EOF
# Managed by codex-switch
model = "${profile_models[$id]}"
model_provider = "$id"
model_reasoning_effort = "${profile_efforts[$id]}"
disable_response_storage = true
service_tier = "default"

[model_providers.$id]
name = "$(profile_label "$id")"
base_url = "$base_url"
env_key = "$key_env"
wire_api = "responses"
EOF
  mv "$temp" "$path"
}

switch_profile() {
  local id="${1:l}" dir="$profiles_root/${1:l}"
  if [[ "$id" == official ]]; then
    launchctl unsetenv CODEX_HOME
    print "Switched to Official ($official_home). Restart Codex / VS Code to apply."; return
  fi
  valid_profile_id "$id" || { print -u2 "Invalid profile name: $1"; return 2; }
  check_profile_safety "$id"
  [[ -n "${profile_base_envs[$id]-}" ]] && write_managed_config "$id"
  [[ -f "$dir/config.toml" ]] || { print -u2 "Profile '$id' has no config.toml."; return 1; }
  launchctl setenv CODEX_HOME "$dir"
  print "Switched to $(profile_label "$id") ($dir). Restart Codex / VS Code to apply."
}

list_profiles() {
  print "Current: $(current_description)"
  print "1) Official ($official_home)"
  print "2) PackyApi ($profiles_root/packyapi)"
  print "3) DevPoolAi ($profiles_root/devpoolai)"
}

menu() {
  list_profiles
  read "choice?Select: "
  case "$choice" in
    1) switch_profile official ;;
    2) switch_profile packyapi ;;
    3) switch_profile devpoolai ;;
    *) print -u2 "Invalid selection."; return 2 ;;
  esac
}

case "${1-}" in
  "") menu ;;
  current) current_description ;;
  official|packyapi|devpoolai) switch_profile "$1" ;;
  list) list_profiles ;;
  *) usage; exit 2 ;;
esac

脚本的关键点不是名字,而是边界:current_description 读取当前 CODEX_HOME;check_profile_safety 防止第三方目录带入 auth.json 并确保 Skills 链接正确;write_managed_config 只从 PACKY_* 或 DEVPOOL_* 环境变量生成配置;switch_profile official 调用 launchctl unsetenv CODEX_HOME 回到官方目录;第三方切换则调用 launchctl setenv,并提示重启 Codex 或 VS Code。脚本不会启动 Codex 本身,也不会把 API Key 写入 TOML。

把上面的脚本粘贴进去后赋予执行权限:

chmod +x "$HOME/.local/bin/codex-switch"
echo "$PATH"

如果输出中没有 $HOME/.local/bin,在 ~/.zshrc 加入:

export PATH="$HOME/.local/bin:$PATH"

然后重新加载:

source "$HOME/.zshrc"
which codex-switch
codex-switch list

如果你把脚本放在 $HOME/bin/codex-switch,请将安装路径和 PATH 示例中的 .local/bin 替换成 $HOME/bin。不要在 Git 仓库中提交真实地址或密钥。

第 4 步:切换并验证

配置环境变量

脚本需要知道 Provider 的地址和 Key 是否存在,但不会把 Key 写进 Profile 文件。示例:

export PACKY_BASE_URL="https://your-provider.example/v1"
export PACKY_API_KEY="YOUR_API_KEY"
export DEVPOOL_BASE_URL="https://your-provider.example/v1"
export DEVPOOL_API_KEY="YOUR_API_KEY"

可以把这些变量放入 ~/.zshrc,也可以放进一个权限收紧的本地 secret 文件后手动加载。无论哪种方式,都不要提交 Git、不要打印到截图、不要放进文章代码块中的真实值。脚本同时检查当前 shell 和 launchctl 环境,所以 GUI 应用启动时也要确认变量已经进入对应的用户环境。

切换 Profile

codex-switch current
codex-switch list
codex-switch packyapi
codex-switch devpoolai
codex-switch official

选择第三方 Profile 后,重启 Codex 或 VS Code;完成 smoke test 后再处理真实项目。恢复官方订阅时运行 codex-switch official,它会清除 CODEX_HOME,让 Codex 回到 $HOME/.codex,但同样需要重启已经打开的应用。

自建 codex-switch 脚本列出 Official、PackyApi 和 DevPoolAi Profile
运行 `codex-switch list`,查看可用的 Official、PackyApi 和 DevPoolAi Profile。

codex-switch 是本地脚本,不是 Codex 官方命令。

恢复官方订阅时运行 codex-switch official,它会清除 CODEX_HOME,让 Codex 回到 $HOME/.codex。切换后需要重启 Codex / VS Code,新的环境才会应用。

使用 codex-switch 切回 Official Profile 并确认当前配置已恢复
运行 `codex-switch official`,确认已切回 Official Profile。

切回 Official 只会清除 CODEX_HOME;第三方请求状态仍需在对应 Profile 中单独验证。

切换后先做最小验证

启动成功不等于请求链路成功。先用一个不包含私密代码的短问题验证:

只回复:provider smoke test passed

同时核对三件事:终端显示的模型是否正确、请求是否命中目标服务、响应是否符合当前 wire_api 的格式。不要把余额、请求 ID 或完整响应中的私人输入放进截图。

Codex 切换 Provider 后完成真实 smoke test 响应
发送最小测试请求,确认切换后的 Provider 返回成功响应。

重点是请求链路实际可用;不要仅凭模型自报名称判断底层 Provider 身份,也不要把这次结果外推为所有兼容服务都可用。

第 5 步:恢复 Official

完成测试后运行 codex-switch official,清除 CODEX_HOME,让 Codex 回到 $HOME/.codex。切换后都需要重启 Codex 或 VS Code,新的环境才会应用。

auth.json 与普通配置的边界

本机 auth.json 存在官方登录模式和 Token 字段;这些值只能留在本机,不能复制到文章、截图或 Provider 配置示例。当前 config.toml 的自定义 Provider 还启用了需要 OpenAI 鉴权的选项,说明鉴权行为取决于 Provider 配置和 Codex 版本;不要把 auth.json 视为第三方通用凭据,也不要假设其行为跨版本一致。

写教程时应明确区分:

  • 官方订阅登录:由 Codex 的官方登录流程和 auth.json 管理。
  • API Key Provider:通常由 Provider 要求的环境变量或安全凭据管理。
  • requires_openai_auth:只按当前版本 schema 和目标服务要求设置,不能为了让配置看起来完整而伪造 auth.json。

Codex 的鉴权行为可能受版本、Provider 与配置方式影响。本文核心方案依赖 Profile 隔离,而不是依赖某种预设的鉴权顺序;官方认证与第三方 Provider 应保持边界,第三方具体鉴权方式以对应 Provider 当前文档为准。不要把 auth.json 复制给第三方 Profile,也不要据此断言所有第三方 Provider 都不使用它。

Skills、AGENTS 和项目配置会不会丢

Provider 只决定模型请求走哪条接口,通常不会改变用户级 skills/、项目内 AGENTS.md 或仓库配置。本文方案通过符号链接让不同 Profile 共用同一套 Skills,依赖的是文件系统层面的共享实现;是否自动发现仍可能受具体 Codex 版本与实际加载机制影响。本文重点是 Profile 隔离与共享目录组织方案,不将其外推为所有版本完全一致的行为。

常见错误

现象 排查方式
401 Unauthorized 检查 Key 是否为空、过期、变量名是否正确,以及请求是否读取了新目录。
403 Forbidden 确认账号具备目标模型权限、来源和套餐允许该 API。
404 或 endpoint 错误 对照服务文档检查根地址;不要把完整 /responses 路径或重复的 /v1 填入 base_url。
model not found 使用服务返回的精确模型 ID,不要直接使用别名或控制台展示名。
配置没有生效 关闭旧进程,检查 CODEX_HOME 和实际加载的配置文件;必要时使用全新临时目录测试。

推荐目录结构

$HOME/.codex/
├── config.toml            # Official 配置
├── auth.json              # 官方登录状态,仅保留在这里
└── skills -> $HOME/.codex-shared/skills

$HOME/.codex-profiles/
├── packyapi/
│   ├── config.toml
│   └── skills -> $HOME/.codex-shared/skills
└── devpoolai/
    ├── config.toml
    └── skills -> $HOME/.codex-shared/skills

Official 的 auth.json 不复制到第三方 Profile;每次切换遵循:选择目录 → 重启 Codex → 核对模型 → 运行 smoke test → 再处理真实任务。任何 Provider 失败,都可以退出当前进程并运行 codex-switch official 回到官方配置。