ChatGPT 桌面端 + Codex CLI 接入
此方式适合同时使用图形化 Codex Agent 和终端 Codex Agent 的用户。地址、Access Key 和下游模型只配置一次,桌面端 Codex、普通 Codex CLI 和 Codex IDE 扩展读取同一份用户级配置。
普通 ChatGPT 对话仍不会使用 Lumoswitch。共享的是 ChatGPT 桌面端中的 Codex 工作区、Codex CLI 和 Codex IDE 扩展。
接入方式一览
| 方式 | 是否提供 | 作用与原因 |
|---|---|---|
| 仅本次启动 | 不提供 | 共用模式必须让图形应用读取用户级配置;需要临时 CLI 时应使用 Codex CLI 隔离配置 |
| 永久使用 | 提供 | 桌面端、CLI 和 IDE 共用同一份提供方、专属模型目录和凭证 |
| 清除接入 | 提供 | 一次清除三端共用的 Lumoswitch 配置,同时保留其他 Codex 设置 |
1. 确认两个入口都可用
- ChatGPT 桌面端可以正常打开并进入 Codex。
- 新终端中运行
codex --version能显示版本。 - Lumoswitch 配置 API 已启用 Responses 输出。
若 codex 命令不存在,先选择一种官方安装方式。npm 方式适用于 Windows、macOS 与 Linux:
# macOS / Linux 独立安装器
curl -fsSL https://chatgpt.com/codex/install.sh | sh
# Windows、macOS 或 Linux 使用 npm
npm install -g @openai/codex
# 或使用 Homebrew
brew install --cask codex安装 ChatGPT 桌面端并不代表已经安装 Codex CLI。
2. 准备四个配置值
配置命令中的占位符需要替换为配置 API 完成页所显示的真实值:
| 占位符 | 填写内容 |
|---|---|
{{api_base_url}} | Lumoswitch API 地址,通常以 /v1 结尾 |
{{access_key}} | 配置 API 的 Access Key,请勿分享或提交到 Git |
{{model}} | 完成页显示的下游模型名称,不是上游平台的展示名称 |
{{codex_model_catalog_base64}} | 按有效能力生成并经 Base64 编码的 Codex 模型目录 |
3. 永久使用(共用配置)
此方式只提供永久配置。临时环境变量无法稳定传给从 Finder、Dock 或 IDE 启动的图形应用。
桌面端单独使用与桌面端 + CLI 共用在各操作系统上必须使用完全相同的配置命令,这样两种方式不会在后续维护中产生不同的合并行为。macOS 中用 Command + Q 完全退出 ChatGPT,结束所有正在运行的 Codex CLI 会话,再替换下面四个占位符并在可信终端中运行完整命令:
set -e
LUMOSWITCH_CODEX_DIR="$HOME/.codex"
LUMOSWITCH_CODEX_CONFIG="$LUMOSWITCH_CODEX_DIR/config.toml"
LUMOSWITCH_CODEX_ENV="$LUMOSWITCH_CODEX_DIR/.env"
LUMOSWITCH_CODEX_CATALOG="$LUMOSWITCH_CODEX_DIR/lumoswitch-model-catalog.json"
LUMOSWITCH_CODEX_BIN="/Applications/ChatGPT.app/Contents/Resources/codex"
LUMOSWITCH_BACKUP_SUFFIX="$(date +%Y%m%d-%H%M%S)-$$"
LUMOSWITCH_CONFIG_TMP=""
LUMOSWITCH_ENV_TMP=""
LUMOSWITCH_CATALOG_TMP=""
lumoswitch_cleanup() {
[ -z "$LUMOSWITCH_CONFIG_TMP" ] || rm -f "$LUMOSWITCH_CONFIG_TMP"
[ -z "$LUMOSWITCH_ENV_TMP" ] || rm -f "$LUMOSWITCH_ENV_TMP"
[ -z "$LUMOSWITCH_CATALOG_TMP" ] || rm -f "$LUMOSWITCH_CATALOG_TMP"
}
trap lumoswitch_cleanup EXIT HUP INT TERM
if pgrep -x ChatGPT >/dev/null 2>&1; then
echo "Quit ChatGPT completely with Command + Q, then run this command again." >&2
exit 1
fi
mkdir -p "$LUMOSWITCH_CODEX_DIR"
umask 077
if [ ! -x "$LUMOSWITCH_CODEX_BIN" ]; then
echo "ChatGPT's bundled Codex executable was not found. Install or update ChatGPT for macOS first." >&2
exit 1
fi
if [ -L "$LUMOSWITCH_CODEX_CONFIG" ] || [ -L "$LUMOSWITCH_CODEX_ENV" ] || [ -L "$LUMOSWITCH_CODEX_CATALOG" ]; then
echo "A Codex settings, credential, or Lumoswitch model-catalog file is a symbolic link. Automatic merge stopped; edit its target manually." >&2
exit 1
fi
if [ -e "$LUMOSWITCH_CODEX_CATALOG" ] && [ ! -f "$LUMOSWITCH_CODEX_CATALOG" ]; then
echo "The Lumoswitch model-catalog path is occupied by a non-regular file. Automatic merge stopped: $LUMOSWITCH_CODEX_CATALOG" >&2
exit 1
fi
LUMOSWITCH_MODEL_VALUE="$(/bin/cat <<'LUMOSWITCH_MODEL_VALUE_EOF'
{{model}}
LUMOSWITCH_MODEL_VALUE_EOF
)"
LUMOSWITCH_CODEX_CATALOG_BASE64="$(/bin/cat <<'LUMOSWITCH_CODEX_CATALOG_BASE64_EOF'
{{codex_model_catalog_base64}}
LUMOSWITCH_CODEX_CATALOG_BASE64_EOF
)"
LUMOSWITCH_MODEL_LITERAL="$(/usr/bin/osascript -l JavaScript - "$LUMOSWITCH_MODEL_VALUE" <<'LUMOSWITCH_STRING_JXA'
function run(argv) {
return JSON.stringify(argv[0]);
}
LUMOSWITCH_STRING_JXA
)"
LUMOSWITCH_CATALOG_LITERAL="$(/usr/bin/osascript -l JavaScript - "$LUMOSWITCH_CODEX_CATALOG" <<'LUMOSWITCH_PATH_JXA'
function run(argv) {
return JSON.stringify(argv[0]);
}
LUMOSWITCH_PATH_JXA
)"
if [ -f "$LUMOSWITCH_CODEX_CONFIG" ]; then
cp "$LUMOSWITCH_CODEX_CONFIG" "$LUMOSWITCH_CODEX_CONFIG.backup.$LUMOSWITCH_BACKUP_SUFFIX"
chmod 600 "$LUMOSWITCH_CODEX_CONFIG.backup.$LUMOSWITCH_BACKUP_SUFFIX"
if awk '
BEGIN { root = 1; found = 0 }
/^[[:space:]]*\[/ { root = 0 }
root && /^[[:space:]]*model_providers[[:space:]]*=/ { found = 1 }
END { exit found ? 0 : 1 }
' "$LUMOSWITCH_CODEX_CONFIG"; then
echo "Inline model_providers detected. Automatic merge stopped; merge it manually. The original file is unchanged." >&2
exit 1
fi
if grep -Eq "^[[:space:]]*('features'([.][^[:space:]=]+)?[[:space:]]*=|\\[[^]]*'features')" "$LUMOSWITCH_CODEX_CONFIG"; then
echo "Single-quoted features settings cannot be merged safely. Automatic merge stopped; the original file is unchanged." >&2
exit 1
fi
if awk '
BEGIN { root = 1; found = 0 }
/^[[:space:]]*\[/ { root = 0 }
root && /^[[:space:]]*(features|"features")([.][^[:space:]=]+)?[[:space:]]*=/ { found = 1 }
END { exit found ? 0 : 1 }
' "$LUMOSWITCH_CODEX_CONFIG"; then
echo "Top-level inline features settings cannot be merged safely into [features]. Automatic merge stopped; the original file is unchanged." >&2
exit 1
fi
if awk '
BEGIN { count = 0; invalid = 0 }
/^[[:space:]]*\[/ {
value = $0
gsub(/[[:space:]]/, "", value)
if (value == "[features]") count++
else if (value ~ /^\[features[.]/ || value ~ /^\["features"/) invalid = 1
}
END { exit count > 1 || invalid ? 0 : 1 }
' "$LUMOSWITCH_CODEX_CONFIG"; then
echo "Duplicate, quoted, or nested features tables cannot be merged safely. Automatic merge stopped; the original file is unchanged." >&2
exit 1
fi
if grep -Eq '^[[:space:]]*\[[[:space:]]*model_providers[.]"lumoswitch"' "$LUMOSWITCH_CODEX_CONFIG"; then
echo "A quoted Lumoswitch provider table was detected. Automatic merge stopped; merge it manually. The original file is unchanged." >&2
exit 1
fi
if awk -v expected="$LUMOSWITCH_CATALOG_LITERAL" '
BEGIN { root = 1; mismatch = 0 }
/^[[:space:]]*\[/ { root = 0 }
root && /^[[:space:]]*model_catalog_json[[:space:]]*=/ {
value = $0
sub(/^[[:space:]]*model_catalog_json[[:space:]]*=[[:space:]]*/, "", value)
sub(/[[:space:]]*(#.*)?$/, "", value)
if (value != expected) mismatch = 1
}
END { exit mismatch ? 0 : 1 }
' "$LUMOSWITCH_CODEX_CONFIG"; then
echo "Another Codex custom model catalog is already configured. Automatic replacement stopped; merge the model manually instead." >&2
exit 1
fi
fi
LUMOSWITCH_CATALOG_TMP="$(mktemp "$LUMOSWITCH_CODEX_DIR/lumoswitch-model-catalog.json.lumoswitch.XXXXXX")"
printf '%s' "$LUMOSWITCH_CODEX_CATALOG_BASE64" | /usr/bin/base64 -D > "$LUMOSWITCH_CATALOG_TMP"
chmod 600 "$LUMOSWITCH_CATALOG_TMP"
LUMOSWITCH_CATALOG_TMP_LITERAL="$(/usr/bin/osascript -l JavaScript - "$LUMOSWITCH_CATALOG_TMP" <<'LUMOSWITCH_TMP_PATH_JXA'
function run(argv) {
return JSON.stringify(argv[0]);
}
LUMOSWITCH_TMP_PATH_JXA
)"
"$LUMOSWITCH_CODEX_BIN" debug models -c "model_catalog_json=$LUMOSWITCH_CATALOG_TMP_LITERAL" >/dev/null
LUMOSWITCH_CONFIG_TMP="$(mktemp "$LUMOSWITCH_CODEX_DIR/config.toml.lumoswitch.XXXXXX")"
{
printf 'model = %s\n' "$LUMOSWITCH_MODEL_LITERAL"
printf 'model_provider = "lumoswitch"\n'
printf 'model_catalog_json = %s\n' "$LUMOSWITCH_CATALOG_LITERAL"
printf 'web_search = "disabled"\n'
if [ -f "$LUMOSWITCH_CODEX_CONFIG" ]; then
printf '\n'
awk '
BEGIN { root = 1; skip = 0; in_features = 0; features_seen = 0 }
/^[[:space:]]*\[/ {
if (in_features) {
print "multi_agent = false"
in_features = 0
}
root = 0
if ($0 ~ /^[[:space:]]*\[[[:space:]]*model_providers[.]lumoswitch([.][^]]+)?[[:space:]]*\]/) {
skip = 1
next
}
skip = 0
if ($0 ~ /^[[:space:]]*\[[[:space:]]*features[[:space:]]*\]/) {
in_features = 1
features_seen = 1
}
}
skip { next }
in_features && /^[[:space:]]*multi_agent[[:space:]]*=/ { next }
root && /^[[:space:]]*(model|model_provider|model_catalog_json|model_reasoning_effort|web_search)[[:space:]]*=/ { next }
{ print }
END {
if (in_features) print "multi_agent = false"
else if (!features_seen) {
print ""
print "[features]"
print "multi_agent = false"
}
}
' "$LUMOSWITCH_CODEX_CONFIG"
else
printf '\n[features]\n'
printf 'multi_agent = false\n'
fi
printf '\n[model_providers.lumoswitch]\n'
printf 'name = "Lumoswitch"\n'
printf 'base_url = "%s"\n' "{{api_base_url}}"
printf 'env_key = "LUMOSWITCH_API_KEY"\n'
printf 'wire_api = "responses"\n'
printf 'http_headers = { "X-Lumoswitch-Agent" = "codex-v1" }\n'
printf 'requires_openai_auth = false\n'
} > "$LUMOSWITCH_CONFIG_TMP"
chmod 600 "$LUMOSWITCH_CONFIG_TMP"
mv "$LUMOSWITCH_CATALOG_TMP" "$LUMOSWITCH_CODEX_CATALOG"
LUMOSWITCH_CATALOG_TMP=""
mv "$LUMOSWITCH_CONFIG_TMP" "$LUMOSWITCH_CODEX_CONFIG"
LUMOSWITCH_CONFIG_TMP=""
if [ -f "$LUMOSWITCH_CODEX_ENV" ]; then
LUMOSWITCH_ENV_TMP="$(mktemp "$LUMOSWITCH_CODEX_DIR/.env.backup.lumoswitch.XXXXXX")"
awk '!/^[[:space:]]*(export[[:space:]]+)?LUMOSWITCH_API_KEY[[:space:]]*=/' "$LUMOSWITCH_CODEX_ENV" > "$LUMOSWITCH_ENV_TMP"
chmod 600 "$LUMOSWITCH_ENV_TMP"
mv "$LUMOSWITCH_ENV_TMP" "$LUMOSWITCH_CODEX_ENV.backup.lumoswitch.$LUMOSWITCH_BACKUP_SUFFIX"
LUMOSWITCH_ENV_TMP=""
fi
LUMOSWITCH_ENV_TMP="$(mktemp "$LUMOSWITCH_CODEX_DIR/.env.lumoswitch.XXXXXX")"
if [ -f "$LUMOSWITCH_CODEX_ENV" ]; then
awk '!/^[[:space:]]*(export[[:space:]]+)?LUMOSWITCH_API_KEY[[:space:]]*=/' "$LUMOSWITCH_CODEX_ENV" > "$LUMOSWITCH_ENV_TMP"
fi
printf 'LUMOSWITCH_API_KEY=%s\n' "{{access_key}}" >> "$LUMOSWITCH_ENV_TMP"
chmod 600 "$LUMOSWITCH_ENV_TMP"
mv "$LUMOSWITCH_ENV_TMP" "$LUMOSWITCH_CODEX_ENV"
LUMOSWITCH_ENV_TMP=""
printf 'Shared Codex settings updated; backup suffix: %s\n' "$LUMOSWITCH_BACKUP_SUFFIX"
open -a ChatGPT该命令会备份并合并 ~/.codex/config.toml,安装由 Lumoswitch 生成且经 ChatGPT 内置 Codex 校验的能力感知目录。目录只开放模型与实际路由共同支持的推理档位,没有可信请求侧控制能力的模型默认使用 none。命令还会设置顶层 web_search = "disabled",并在已有或新建的 [features] 中设置 multi_agent = false,关闭 Codex 多智能体 namespace 与原生网页搜索;普通函数工具、终端命令和文件操作仍可使用。Access Key 会写入权限为 0600 的 ~/.codex/.env,其他现有配置会保留;遇到无法安全合并的配置时,命令会停止而不会静默覆盖。
Windows 原生 PowerShell 导入
当 ChatGPT 使用默认的 Windows 原生 Agent 环境时,Windows 桌面端、原生 Codex CLI 和 IDE 扩展会共享 %USERPROFILE%\.codex 中的用户级 Codex 配置。如果 设置 > Agent 环境 已切换为 WSL,请先改回 Windows 原生 并重启 ChatGPT,因为 WSL 默认使用另一套 Codex 主目录。完全退出 ChatGPT 并结束所有 Codex CLI 会话后,在可信的 PowerShell 终端中运行以下完整命令。
& {
$ErrorActionPreference = 'Stop'
$lumoswitchCodexDir = Join-Path $HOME '.codex'
$lumoswitchConfig = Join-Path $lumoswitchCodexDir 'config.toml'
$lumoswitchEnvironment = Join-Path $lumoswitchCodexDir '.env'
$lumoswitchCatalog = Join-Path $lumoswitchCodexDir 'lumoswitch-model-catalog.json'
$lumoswitchBackupSuffix = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + $PID
$lumoswitchChatGptApp = Get-StartApps | Where-Object { $_.Name -eq 'ChatGPT' -or $_.AppID -match 'ChatGPT' } | Select-Object -First 1
if ($null -eq $lumoswitchChatGptApp) { throw 'ChatGPT was not found in the Start menu. Install or update the Windows desktop app first.' }
if (Get-Process -Name 'ChatGPT' -ErrorAction SilentlyContinue) { throw 'Quit ChatGPT completely, then run this command again.' }
function Assert-LumoswitchFile([string] $Path) {
if (-not (Test-Path -LiteralPath $Path)) { return }
$lumoswitchItem = Get-Item -LiteralPath $Path -Force
if ($lumoswitchItem.PSIsContainer -or ($lumoswitchItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw "Lumoswitch refused to replace a directory or link: $Path"
}
}
function Protect-LumoswitchFile([string] $Path) {
$lumoswitchSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
& icacls.exe $Path '/inheritance:r' '/grant:r' ('*' + $lumoswitchSid + ':(F)') '*S-1-5-18:(F)' '*S-1-5-32-544:(F)' | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Could not restrict access to $Path" }
}
function Write-LumoswitchFile([string] $Path, [string] $Content) {
Assert-LumoswitchFile $Path
[IO.Directory]::CreateDirectory((Split-Path -Parent $Path)) | Out-Null
$lumoswitchTemporary = $Path + '.' + [Guid]::NewGuid().ToString('N') + '.tmp'
try {
[IO.File]::WriteAllText($lumoswitchTemporary, $Content, [Text.UTF8Encoding]::new($false))
Move-Item -LiteralPath $lumoswitchTemporary -Destination $Path -Force
Protect-LumoswitchFile $Path
} finally {
Remove-Item -LiteralPath $lumoswitchTemporary -Force -ErrorAction SilentlyContinue
}
}
if (Test-Path -LiteralPath $lumoswitchCodexDir) {
$lumoswitchRootItem = Get-Item -LiteralPath $lumoswitchCodexDir -Force
if (-not $lumoswitchRootItem.PSIsContainer -or ($lumoswitchRootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw 'The user-level Codex directory is not a regular directory. Automatic merge stopped.'
}
} else {
[IO.Directory]::CreateDirectory($lumoswitchCodexDir) | Out-Null
}
foreach ($lumoswitchPath in @($lumoswitchConfig, $lumoswitchEnvironment, $lumoswitchCatalog)) { Assert-LumoswitchFile $lumoswitchPath }
$lumoswitchCatalogText = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{{codex_model_catalog_base64}}'))
$lumoswitchCatalogObject = $lumoswitchCatalogText | ConvertFrom-Json
$lumoswitchCatalogModels = @($lumoswitchCatalogObject.models)
if ($lumoswitchCatalogModels.Count -eq 0 -or @($lumoswitchCatalogModels.slug) -notcontains '{{model}}') {
throw 'The generated Codex model catalog is empty or does not contain the selected model.'
}
$lumoswitchCatalogToml = $lumoswitchCatalog.Replace('\', '/')
$lumoswitchExpectedCatalog = '"' + $lumoswitchCatalogToml + '"'
$lumoswitchConfigLines = if (Test-Path -LiteralPath $lumoswitchConfig -PathType Leaf) { @([IO.File]::ReadAllLines($lumoswitchConfig, [Text.Encoding]::UTF8)) } else { @() }
$lumoswitchRoot = $true
$lumoswitchFeaturesCount = 0
foreach ($lumoswitchLine in $lumoswitchConfigLines) {
if ($lumoswitchLine -match '^\s*\[') {
$lumoswitchRoot = $false
$lumoswitchNormalized = $lumoswitchLine -replace '\s', ''
if ($lumoswitchNormalized -eq '[features]') { $lumoswitchFeaturesCount++ }
elseif ($lumoswitchNormalized -match '^\[(?:"features"|features\.)') { throw 'Quoted or nested features tables cannot be merged safely.' }
if ($lumoswitchNormalized -match '^\[model_providers\."lumoswitch"') { throw 'A quoted Lumoswitch provider table cannot be merged safely.' }
}
if ($lumoswitchRoot -and $lumoswitchLine -match '^\s*model_providers\s*=') { throw 'Inline model_providers cannot be merged safely.' }
if ($lumoswitchRoot -and $lumoswitchLine -match '^\s*features(?:\.|\s*=)') { throw 'Inline features settings cannot be merged safely.' }
if ($lumoswitchRoot -and $lumoswitchLine -match '^\s*model_catalog_json\s*=') {
$lumoswitchValue = ($lumoswitchLine -replace '^\s*model_catalog_json\s*=\s*', '') -replace '\s*(#.*)?$', ''
if ($lumoswitchValue -ne $lumoswitchExpectedCatalog) { throw 'Another Codex custom model catalog is already configured.' }
}
}
if ($lumoswitchFeaturesCount -gt 1) { throw 'Duplicate features tables cannot be merged safely.' }
if (Test-Path -LiteralPath $lumoswitchConfig -PathType Leaf) {
$lumoswitchConfigBackup = $lumoswitchConfig + '.backup.' + $lumoswitchBackupSuffix
Assert-LumoswitchFile $lumoswitchConfigBackup
Copy-Item -LiteralPath $lumoswitchConfig -Destination $lumoswitchConfigBackup
Protect-LumoswitchFile $lumoswitchConfigBackup
}
$lumoswitchOutput = [Collections.Generic.List[string]]::new()
foreach ($lumoswitchLine in @(
'model = "{{model}}"',
'model_provider = "lumoswitch"',
('model_catalog_json = "' + $lumoswitchCatalogToml + '"'),
'web_search = "disabled"',
''
)) { $lumoswitchOutput.Add($lumoswitchLine) | Out-Null }
$lumoswitchCopyRoot = $true
$lumoswitchSkip = $false
$lumoswitchInFeatures = $false
$lumoswitchFeaturesSeen = $false
foreach ($lumoswitchLine in $lumoswitchConfigLines) {
if ($lumoswitchLine -match '^\s*\[') {
if ($lumoswitchInFeatures) { $lumoswitchOutput.Add('multi_agent = false') | Out-Null }
$lumoswitchInFeatures = $false
$lumoswitchCopyRoot = $false
if ($lumoswitchLine -match '^\s*\[\s*model_providers\.lumoswitch(?:\.[^]]+)?\s*\]\s*$') { $lumoswitchSkip = $true; continue }
$lumoswitchSkip = $false
if (($lumoswitchLine -replace '\s', '') -eq '[features]') { $lumoswitchInFeatures = $true; $lumoswitchFeaturesSeen = $true }
}
if ($lumoswitchSkip) { continue }
if ($lumoswitchInFeatures -and $lumoswitchLine -match '^\s*multi_agent\s*=') { continue }
if ($lumoswitchCopyRoot -and $lumoswitchLine -match '^\s*(model|model_provider|model_catalog_json|model_reasoning_effort|web_search)\s*=') { continue }
$lumoswitchOutput.Add($lumoswitchLine) | Out-Null
}
if ($lumoswitchInFeatures) { $lumoswitchOutput.Add('multi_agent = false') | Out-Null }
if (-not $lumoswitchFeaturesSeen) {
$lumoswitchOutput.Add('') | Out-Null
$lumoswitchOutput.Add('[features]') | Out-Null
$lumoswitchOutput.Add('multi_agent = false') | Out-Null
}
foreach ($lumoswitchLine in @(
'',
'[model_providers.lumoswitch]',
'name = "Lumoswitch"',
'base_url = "{{api_base_url}}"',
'env_key = "LUMOSWITCH_API_KEY"',
'wire_api = "responses"',
'http_headers = { "X-Lumoswitch-Agent" = "codex-v1" }',
'requires_openai_auth = false'
)) { $lumoswitchOutput.Add($lumoswitchLine) | Out-Null }
$lumoswitchNewLine = [Environment]::NewLine
Write-LumoswitchFile $lumoswitchCatalog $lumoswitchCatalogText
Write-LumoswitchFile $lumoswitchConfig (($lumoswitchOutput -join $lumoswitchNewLine) + $lumoswitchNewLine)
$lumoswitchEnvironmentLines = if (Test-Path -LiteralPath $lumoswitchEnvironment -PathType Leaf) {
@([IO.File]::ReadAllLines($lumoswitchEnvironment, [Text.Encoding]::UTF8) | Where-Object { $_ -notmatch '^\s*(?:export\s+)?LUMOSWITCH_API_KEY\s*=' })
} else { @() }
if (Test-Path -LiteralPath $lumoswitchEnvironment -PathType Leaf) {
$lumoswitchEnvironmentBackup = $lumoswitchEnvironment + '.backup.lumoswitch.' + $lumoswitchBackupSuffix
$lumoswitchEnvironmentBackupText = if ($lumoswitchEnvironmentLines.Count -gt 0) { ($lumoswitchEnvironmentLines -join $lumoswitchNewLine) + $lumoswitchNewLine } else { '' }
Write-LumoswitchFile $lumoswitchEnvironmentBackup $lumoswitchEnvironmentBackupText
}
$lumoswitchEnvironmentLines += 'LUMOSWITCH_API_KEY={{access_key}}'
Write-LumoswitchFile $lumoswitchEnvironment (($lumoswitchEnvironmentLines -join $lumoswitchNewLine) + $lumoswitchNewLine)
Write-Host ('Shared Codex settings updated; backup suffix: ' + $lumoswitchBackupSuffix)
Start-Process -FilePath explorer.exe -ArgumentList @('shell:AppsFolder\' + $lumoswitchChatGptApp.AppID)
}命令会动态发现 ChatGPT 的开始菜单 App ID,不依赖固定的 Microsoft Store 包名;写入前验证模型目录和目标模型,按与 macOS 相同的保守规则合并 TOML,并用 Windows ACL 限制模型目录、配置、凭证和备份文件的访问权限。
4. 日常启动
ChatGPT 桌面端
像平时一样打开 ChatGPT,再进入 Codex;也可以运行:
open -a ChatGPTCodex CLI
进入目标项目目录,直接运行:
codex在 CLI 中运行 /status 可以确认当前模型和提供方。
新任务使用生成目录中的有效默认档位。/reasoning 只列出经过校验的档位;DeepSeek 这类开关式推理模型只显示一个等效的开启档位。
Codex IDE 扩展
修改配置后完全重启 IDE 或重新加载 Codex 扩展,再新建任务。IDE 扩展直接读取相同的 ~/.codex 配置,不需要维护另一份命令。
| 使用入口 | 读取的配置 | 配置变化后怎么做 |
|---|---|---|
| 桌面端 Codex | 配置、凭证与 Lumoswitch 模型目录 | 完全退出 ChatGPT 后重新打开 |
| 普通 Codex CLI | 相同的三个文件 | 退出旧会话,在新终端运行 codex |
| Codex IDE 扩展 | 相同的三个文件 | 完全重启 IDE,再新建任务 |
| 普通 ChatGPT 对话 | 不读取 | 不受影响 |
更换 Access Key、地址或下游模型名称后,重新运行本页的永久使用命令,并完全重启正在使用的 Codex 客户端。桌面端和 CLI 的模型选择器都应只显示完成页给出的下游模型;发送请求后,再确认 Lumoswitch 日志中的模型名称完全一致。
5. 清除接入
清除会同时影响 ChatGPT 桌面端中的 Codex、普通 Codex CLI 和 Codex IDE 扩展。
macOS
先用 Command + Q 完全退出 ChatGPT,并结束所有 Codex CLI 会话,再运行:
set -e
LUMOSWITCH_CODEX_DIR="$HOME/.codex"
LUMOSWITCH_CODEX_CONFIG="$LUMOSWITCH_CODEX_DIR/config.toml"
LUMOSWITCH_CODEX_ENV="$LUMOSWITCH_CODEX_DIR/.env"
LUMOSWITCH_CODEX_CATALOG="$LUMOSWITCH_CODEX_DIR/lumoswitch-model-catalog.json"
LUMOSWITCH_BACKUP_SUFFIX="$(date +%Y%m%d-%H%M%S)-remove-$$"
LUMOSWITCH_CONFIG_TMP=""
LUMOSWITCH_ENV_TMP=""
lumoswitch_cleanup() {
[ -z "$LUMOSWITCH_CONFIG_TMP" ] || rm -f "$LUMOSWITCH_CONFIG_TMP"
[ -z "$LUMOSWITCH_ENV_TMP" ] || rm -f "$LUMOSWITCH_ENV_TMP"
}
trap lumoswitch_cleanup EXIT HUP INT TERM
if pgrep -x ChatGPT >/dev/null 2>&1; then
echo "请先用 Command + Q 完全退出 ChatGPT,再重新运行此命令。" >&2
exit 1
fi
mkdir -p "$LUMOSWITCH_CODEX_DIR"
umask 077
if [ -L "$LUMOSWITCH_CODEX_CONFIG" ] || [ -L "$LUMOSWITCH_CODEX_ENV" ] || [ -L "$LUMOSWITCH_CODEX_CATALOG" ]; then
echo "检测到 Codex 配置、凭证或 Lumoswitch 模型目录是符号链接,已停止自动清除。请手动修改链接目标。" >&2
exit 1
fi
if [ -e "$LUMOSWITCH_CODEX_CATALOG" ] && [ ! -f "$LUMOSWITCH_CODEX_CATALOG" ]; then
echo "Lumoswitch 模型目录路径已被非普通文件占用,已停止自动清除:$LUMOSWITCH_CODEX_CATALOG" >&2
exit 1
fi
LUMOSWITCH_CATALOG_LITERAL="$(/usr/bin/osascript -l JavaScript - "$LUMOSWITCH_CODEX_CATALOG" <<'LUMOSWITCH_REMOVE_PATH_JXA'
function run(argv) {
return JSON.stringify(argv[0]);
}
LUMOSWITCH_REMOVE_PATH_JXA
)"
LUMOSWITCH_REMOVE_DEFAULTS=0
if [ -f "$LUMOSWITCH_CODEX_CONFIG" ] && awk '
BEGIN { root = 1; found = 0 }
/^[[:space:]]*\[/ { root = 0 }
root && /^[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"lumoswitch"[[:space:]]*(#.*)?$/ { found = 1 }
END { exit found ? 0 : 1 }
' "$LUMOSWITCH_CODEX_CONFIG"; then
LUMOSWITCH_REMOVE_DEFAULTS=1
fi
LUMOSWITCH_REMOVE_CATALOG_SETTING=0
if [ -f "$LUMOSWITCH_CODEX_CONFIG" ] && awk -v expected="$LUMOSWITCH_CATALOG_LITERAL" '
BEGIN { root = 1; found = 0 }
/^[[:space:]]*\[/ { root = 0 }
root && /^[[:space:]]*model_catalog_json[[:space:]]*=/ {
value = $0
sub(/^[[:space:]]*model_catalog_json[[:space:]]*=[[:space:]]*/, "", value)
sub(/[[:space:]]*(#.*)?$/, "", value)
if (value == expected) found = 1
}
END { exit found ? 0 : 1 }
' "$LUMOSWITCH_CODEX_CONFIG"; then
LUMOSWITCH_REMOVE_CATALOG_SETTING=1
fi
if [ -f "$LUMOSWITCH_CODEX_CONFIG" ]; then
cp "$LUMOSWITCH_CODEX_CONFIG" "$LUMOSWITCH_CODEX_CONFIG.backup.$LUMOSWITCH_BACKUP_SUFFIX"
chmod 600 "$LUMOSWITCH_CODEX_CONFIG.backup.$LUMOSWITCH_BACKUP_SUFFIX"
LUMOSWITCH_CONFIG_TMP="$(mktemp "$LUMOSWITCH_CODEX_DIR/config.toml.lumoswitch-remove.XXXXXX")"
awk -v remove_defaults="$LUMOSWITCH_REMOVE_DEFAULTS" -v remove_catalog="$LUMOSWITCH_REMOVE_CATALOG_SETTING" '
BEGIN { root = 1; skip = 0; in_features = 0 }
/^[[:space:]]*\[/ {
root = 0
in_features = ($0 ~ /^[[:space:]]*\[[[:space:]]*features[[:space:]]*\]/)
if ($0 ~ /^[[:space:]]*\[[[:space:]]*model_providers[.]lumoswitch([.][^]]+)?[[:space:]]*\]/) {
skip = 1
next
}
skip = 0
}
skip { next }
in_features && remove_defaults == "1" && /^[[:space:]]*multi_agent[[:space:]]*=[[:space:]]*false[[:space:]]*(#.*)?$/ { next }
root && remove_defaults == "1" && /^[[:space:]]*(model|model_provider|model_reasoning_effort)[[:space:]]*=/ { next }
root && remove_defaults == "1" && /^[[:space:]]*web_search[[:space:]]*=[[:space:]]*"disabled"[[:space:]]*(#.*)?$/ { next }
root && remove_catalog == "1" && /^[[:space:]]*model_catalog_json[[:space:]]*=/ { next }
{ print }
' "$LUMOSWITCH_CODEX_CONFIG" > "$LUMOSWITCH_CONFIG_TMP"
chmod 600 "$LUMOSWITCH_CONFIG_TMP"
mv "$LUMOSWITCH_CONFIG_TMP" "$LUMOSWITCH_CODEX_CONFIG"
LUMOSWITCH_CONFIG_TMP=""
fi
if [ "$LUMOSWITCH_REMOVE_CATALOG_SETTING" = "1" ] && [ -f "$LUMOSWITCH_CODEX_CATALOG" ]; then
rm -f "$LUMOSWITCH_CODEX_CATALOG"
fi
if [ -f "$LUMOSWITCH_CODEX_ENV" ]; then
LUMOSWITCH_ENV_TMP="$(mktemp "$LUMOSWITCH_CODEX_DIR/.env.backup.lumoswitch.XXXXXX")"
awk '!/^[[:space:]]*(export[[:space:]]+)?LUMOSWITCH_API_KEY[[:space:]]*=/' "$LUMOSWITCH_CODEX_ENV" > "$LUMOSWITCH_ENV_TMP"
chmod 600 "$LUMOSWITCH_ENV_TMP"
mv "$LUMOSWITCH_ENV_TMP" "$LUMOSWITCH_CODEX_ENV.backup.lumoswitch.$LUMOSWITCH_BACKUP_SUFFIX"
LUMOSWITCH_ENV_TMP=""
LUMOSWITCH_ENV_TMP="$(mktemp "$LUMOSWITCH_CODEX_DIR/.env.lumoswitch-remove.XXXXXX")"
awk '!/^[[:space:]]*(export[[:space:]]+)?LUMOSWITCH_API_KEY[[:space:]]*=/' "$LUMOSWITCH_CODEX_ENV" > "$LUMOSWITCH_ENV_TMP"
chmod 600 "$LUMOSWITCH_ENV_TMP"
mv "$LUMOSWITCH_ENV_TMP" "$LUMOSWITCH_CODEX_ENV"
LUMOSWITCH_ENV_TMP=""
fi
printf 'Lumoswitch Codex 接入已清除;清除前备份后缀:%s\n' "$LUMOSWITCH_BACKUP_SUFFIX"
open -a ChatGPT命令会先生成不含 Lumoswitch Key 的安全备份,再只移除 Lumoswitch 相关配置。如果顶层 model_provider 仍指向 lumoswitch,它会同时移除顶层模型、提供方和旧版接入留下的 model_reasoning_effort;只有 web_search 仍精确等于 "disabled"、[features] 中的 multi_agent 仍精确等于 false 时才移除这两个兼容值,用户后来改过的值和其他 feature 项都会保留,空的 [features] 表也可以保留。只有 model_catalog_json 精确指向 Lumoswitch 专属文件时,才会移除该键并删除该普通文件;其他自定义模型目录不会被碰触。符号链接不会被跟随。自动清除无法知道接入前被覆盖的默认值;需要精确恢复时,只能在确认接入后没有其他改动的前提下,手动恢复安装命令生成的同一 config.toml 后缀备份。Access Key 被有意排除在备份之外,重新接入时需要重新填写。
Windows PowerShell
清理会同时影响三个共用入口。完全退出 ChatGPT 并结束所有 Codex CLI 会话,然后在 PowerShell 中运行:
& {
$ErrorActionPreference = 'Stop'
$lumoswitchCodexDir = Join-Path $HOME '.codex'
$lumoswitchConfig = Join-Path $lumoswitchCodexDir 'config.toml'
$lumoswitchEnvironment = Join-Path $lumoswitchCodexDir '.env'
$lumoswitchCatalog = Join-Path $lumoswitchCodexDir 'lumoswitch-model-catalog.json'
$lumoswitchBackupSuffix = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-remove-' + $PID
if (Get-Process -Name 'ChatGPT' -ErrorAction SilentlyContinue) { throw 'Quit ChatGPT completely, then run this command again.' }
$lumoswitchChatGptApp = Get-StartApps | Where-Object { $_.Name -eq 'ChatGPT' -or $_.AppID -match 'ChatGPT' } | Select-Object -First 1
function Assert-LumoswitchFile([string] $Path) {
if (-not (Test-Path -LiteralPath $Path)) { return }
$lumoswitchItem = Get-Item -LiteralPath $Path -Force
if ($lumoswitchItem.PSIsContainer -or ($lumoswitchItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw "Lumoswitch refused to modify a directory or link: $Path"
}
}
function Protect-LumoswitchFile([string] $Path) {
$lumoswitchSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
& icacls.exe $Path '/inheritance:r' '/grant:r' ('*' + $lumoswitchSid + ':(F)') '*S-1-5-18:(F)' '*S-1-5-32-544:(F)' | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Could not restrict access to $Path" }
}
function Write-LumoswitchFile([string] $Path, [string] $Content) {
Assert-LumoswitchFile $Path
[IO.Directory]::CreateDirectory((Split-Path -Parent $Path)) | Out-Null
$lumoswitchTemporary = $Path + '.' + [Guid]::NewGuid().ToString('N') + '.tmp'
try {
[IO.File]::WriteAllText($lumoswitchTemporary, $Content, [Text.UTF8Encoding]::new($false))
Move-Item -LiteralPath $lumoswitchTemporary -Destination $Path -Force
Protect-LumoswitchFile $Path
} finally {
Remove-Item -LiteralPath $lumoswitchTemporary -Force -ErrorAction SilentlyContinue
}
}
if (Test-Path -LiteralPath $lumoswitchCodexDir) {
$lumoswitchRootItem = Get-Item -LiteralPath $lumoswitchCodexDir -Force
if (-not $lumoswitchRootItem.PSIsContainer -or ($lumoswitchRootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw 'The user-level Codex directory is not a regular directory. Automatic removal stopped.'
}
} else {
[IO.Directory]::CreateDirectory($lumoswitchCodexDir) | Out-Null
}
foreach ($lumoswitchPath in @($lumoswitchConfig, $lumoswitchEnvironment, $lumoswitchCatalog)) { Assert-LumoswitchFile $lumoswitchPath }
$lumoswitchNewLine = [Environment]::NewLine
$lumoswitchConfigLines = if (Test-Path -LiteralPath $lumoswitchConfig -PathType Leaf) { @([IO.File]::ReadAllLines($lumoswitchConfig, [Text.Encoding]::UTF8)) } else { @() }
$lumoswitchExpectedCatalog = '"' + $lumoswitchCatalog.Replace('\', '/') + '"'
$lumoswitchRemoveDefaults = $false
$lumoswitchCatalogSettings = 0
$lumoswitchCatalogMatches = 0
$lumoswitchRoot = $true
foreach ($lumoswitchLine in $lumoswitchConfigLines) {
if ($lumoswitchLine -match '^\s*\[') { $lumoswitchRoot = $false }
if ($lumoswitchRoot -and $lumoswitchLine -match '^\s*model_provider\s*=\s*"lumoswitch"\s*(?:#.*)?$') { $lumoswitchRemoveDefaults = $true }
if ($lumoswitchRoot -and $lumoswitchLine -match '^\s*model_catalog_json\s*=') {
$lumoswitchCatalogSettings++
$lumoswitchValue = ($lumoswitchLine -replace '^\s*model_catalog_json\s*=\s*', '') -replace '\s*(#.*)?$', ''
if ($lumoswitchValue -eq $lumoswitchExpectedCatalog) { $lumoswitchCatalogMatches++ }
}
}
$lumoswitchRemoveCatalog = ($lumoswitchCatalogSettings -eq 1 -and $lumoswitchCatalogMatches -eq 1)
if (Test-Path -LiteralPath $lumoswitchConfig -PathType Leaf) {
$lumoswitchConfigBackup = $lumoswitchConfig + '.backup.' + $lumoswitchBackupSuffix
Assert-LumoswitchFile $lumoswitchConfigBackup
Copy-Item -LiteralPath $lumoswitchConfig -Destination $lumoswitchConfigBackup
Protect-LumoswitchFile $lumoswitchConfigBackup
$lumoswitchOutput = [Collections.Generic.List[string]]::new()
$lumoswitchCopyRoot = $true
$lumoswitchSkip = $false
$lumoswitchInFeatures = $false
foreach ($lumoswitchLine in $lumoswitchConfigLines) {
if ($lumoswitchLine -match '^\s*\[') {
$lumoswitchCopyRoot = $false
$lumoswitchInFeatures = (($lumoswitchLine -replace '\s', '') -eq '[features]')
if ($lumoswitchLine -match '^\s*\[\s*model_providers\.lumoswitch(?:\.[^]]+)?\s*\]\s*$') { $lumoswitchSkip = $true; continue }
$lumoswitchSkip = $false
}
if ($lumoswitchSkip) { continue }
if ($lumoswitchInFeatures -and $lumoswitchRemoveDefaults -and $lumoswitchLine -match '^\s*multi_agent\s*=\s*false\s*(?:#.*)?$') { continue }
if ($lumoswitchCopyRoot -and $lumoswitchRemoveDefaults -and $lumoswitchLine -match '^\s*(?:model|model_provider|model_reasoning_effort)\s*=') { continue }
if ($lumoswitchCopyRoot -and $lumoswitchRemoveDefaults -and $lumoswitchLine -match '^\s*web_search\s*=\s*"disabled"\s*(?:#.*)?$') { continue }
if ($lumoswitchCopyRoot -and $lumoswitchRemoveCatalog -and $lumoswitchLine -match '^\s*model_catalog_json\s*=') { continue }
$lumoswitchOutput.Add($lumoswitchLine) | Out-Null
}
$lumoswitchConfigText = if ($lumoswitchOutput.Count -gt 0) { ($lumoswitchOutput -join $lumoswitchNewLine) + $lumoswitchNewLine } else { '' }
Write-LumoswitchFile $lumoswitchConfig $lumoswitchConfigText
}
if ($lumoswitchRemoveCatalog -and (Test-Path -LiteralPath $lumoswitchCatalog -PathType Leaf)) {
Remove-Item -LiteralPath $lumoswitchCatalog -Force
}
if (Test-Path -LiteralPath $lumoswitchEnvironment -PathType Leaf) {
$lumoswitchEnvironmentLines = @([IO.File]::ReadAllLines($lumoswitchEnvironment, [Text.Encoding]::UTF8) | Where-Object { $_ -notmatch '^\s*(?:export\s+)?LUMOSWITCH_API_KEY\s*=' })
$lumoswitchEnvironmentText = if ($lumoswitchEnvironmentLines.Count -gt 0) { ($lumoswitchEnvironmentLines -join $lumoswitchNewLine) + $lumoswitchNewLine } else { '' }
$lumoswitchEnvironmentBackup = $lumoswitchEnvironment + '.backup.lumoswitch.' + $lumoswitchBackupSuffix
Write-LumoswitchFile $lumoswitchEnvironmentBackup $lumoswitchEnvironmentText
Write-LumoswitchFile $lumoswitchEnvironment $lumoswitchEnvironmentText
}
Write-Host ('Lumoswitch Codex setup removed; pre-removal backup suffix: ' + $lumoswitchBackupSuffix)
if ($null -ne $lumoswitchChatGptApp) {
Start-Process -FilePath explorer.exe -ArgumentList @('shell:AppsFolder\' + $lumoswitchChatGptApp.AppID)
}
}Windows 命令会对 %USERPROFILE%\\.codex 应用同样的保守清理规则,生成不含 Access Key 且受 ACL 保护的备份;如果系统中仍安装 ChatGPT,则通过动态发现的开始菜单 App ID 重新打开。
6. 什么时候不要共用
如果你希望桌面端继续使用原来的提供方,而只有 CLI 使用 Lumoswitch,请使用 Codex CLI 原生接入。它通过专属 CODEX_HOME 隔离终端配置,不修改共享配置。
如果你目前只使用桌面端,也没有安装 CLI,请使用更简单的 ChatGPT 桌面端(Codex)接入。两种场景底层使用同一个永久配置模板,但桌面端专页不会要求安装或验证 CLI。
发布维护字段
- 平台 ID:
codex-desktop-cli - 展示名称:
ChatGPT Desktop + Codex CLI - 下游协议:
responses - macOS/Linux 临时模式:关闭;
commandTemplate留空 - macOS 永久策略:
native persistentCommandTemplate:复制本页「永久使用(共用配置)」代码框的完整内容;发布前确认它与chatgpt-desktop记录逐字一致- macOS
futureCommand:codex - Windows 临时模式:关闭;
windowsCommandTemplate留空 - Windows 永久策略:
native windowsPersistentCommandTemplate:复制本页「Windows 原生 PowerShell 导入」代码框的完整内容;发布前确认它与chatgpt-desktop的 Windows 记录逐字一致- Windows
windowsFutureCommand:codex - 展示顺序:
16 - 发布限制:macOS 与 Windows 模板只发布到各自标记的操作系统,并在每个平台分别验证桌面端和 CLI
- 核验状态:官方配置契约已于
2026-08-27重新核验;最近一次使用 ChatGPT 内置 Codex 验证专属目录只返回指定模型是在2026-08-03;Windows PowerShell 已有结构与双语模板契约测试,真实 Windows 双端 Lumoswitch 流量仍待复核
常见问题
- 桌面端生效但 CLI 没生效:完全退出旧 CLI 会话,在新终端运行普通
codex;不要继续使用旧的 Lumoswitch 专属启动器测试共用模式。 - CLI 生效但桌面端没生效:完全退出 ChatGPT(macOS 使用
Command + Q,Windows 从应用菜单退出)后重新打开。 - 模型选择器仍只有 GPT 模型:重新运行永久命令,确认没有“其他自定义模型目录”提示,再完全重启两端。
- 两端显示不同模型:确认项目级
.codex/config.toml或 CLI 启动参数没有覆盖用户级模型目录和默认模型。 - 仍提示
The inference request is invalid:先确认模型名称和 Responses 输出,再重新执行一键导入刷新能力目录;仍失败时使用对应的 Lumoswitch 请求日志定位被拒绝的字段。 - 需要恢复原配置:使用配置命令输出的备份后缀,把对应
.backup.<后缀>文件复制回原路径。
参考:ChatGPT Windows 桌面端、Codex CLI、Codex 自定义模型提供方、Codex 模型目录和桌面端与 IDE 环境变量。