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

最终你会得到什么
完成本文后,你会保留独立的 Official 配置,并能用单独的 Profile 接入 OpenAI Compatible API;切换、验证和恢复都通过 codex-switch 完成,不会覆盖官方登录状态。
主流程只有 5 步:
- 配置 Provider
- 创建独立 Profile
- 安装
codex-switch - 切换并验证
- 恢复 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 = falseAPI Key 不放进这个示例文件。优先使用该版本支持的环境变量或系统密钥存储;变量名也必须以 Provider 文档为准。不要把 auth.json 当成第三方 API Key 的通用容器。

截图展示了真实配置中的 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 是本地脚本,不是 Codex 官方命令。
恢复官方订阅时运行 codex-switch official,它会清除 CODEX_HOME,让 Codex 回到 $HOME/.codex。切换后需要重启 Codex / VS Code,新的环境才会应用。

切回 Official 只会清除 CODEX_HOME;第三方请求状态仍需在对应 Profile 中单独验证。
切换后先做最小验证
启动成功不等于请求链路成功。先用一个不包含私密代码的短问题验证:
只回复:provider smoke test passed同时核对三件事:终端显示的模型是否正确、请求是否命中目标服务、响应是否符合当前 wire_api 的格式。不要把余额、请求 ID 或完整响应中的私人输入放进截图。

重点是请求链路实际可用;不要仅凭模型自报名称判断底层 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/skillsOfficial 的 auth.json 不复制到第三方 Profile;每次切换遵循:选择目录 → 重启 Codex → 核对模型 → 运行 smoke test → 再处理真实任务。任何 Provider 失败,都可以退出当前进程并运行 codex-switch official 回到官方配置。