Lumoswitch 文档

ChatGPT 桌面端(Codex)接入

ChatGPT 桌面端中的 Codex 是面向本地项目的图形化编码 Agent。此方式适合已经安装 macOS 或 Windows 版 ChatGPT 桌面端、平时只在应用中使用 Codex 的用户。你不需要安装 codex 命令;Terminal 或 PowerShell 只在写入配置时使用一次。

接入后只有 Codex 工作区使用 Lumoswitch。普通 ChatGPT 对话、语音和其他 ChatGPT 功能仍使用原来的服务。

接入方式一览

方式是否提供作用与原因
仅本次启动不提供桌面启动器没有受支持的单次配置参数,也不会可靠继承 Shell 或 PowerShell 变量
永久使用提供安全合并用户级 Codex 配置,桌面端、CLI 和 IDE 都会读取
清除接入提供只移除 Lumoswitch 提供方、默认指向和专用 Key,保留其他 Codex 设置

1. 准备配置 API

配置 API 需要提供:

  • Responses 输出;
  • 一个有效的 Lumoswitch Access Key;
  • 完成页显示的下游模型名称。

以下命令中的占位符需要替换为完成页所显示的真实值:

占位符填写内容
{{api_base_url}}Lumoswitch API 地址,通常以 /v1 结尾
{{access_key}}配置 API 的 Access Key,请勿分享或提交到 Git
{{model}}完成页显示的下游模型名称,不是上游平台的展示名称
{{codex_model_catalog_base64}}按有效能力生成并经 Base64 编码的 Codex 模型目录

如果 Access Key 曾出现在公开聊天、文档、截图或代码仓库中,请先轮换再继续。

2. 完全退出 ChatGPT

macOS 中选择 ChatGPT > Quit ChatGPT 或按 Command + Q;Windows 中从应用菜单退出,并确认没有残留 ChatGPT 进程。只关闭窗口可能不会重新加载提供方和凭证。

3. 永久使用

此方式不提供临时命令。从 Finder、Dock、开始菜单或登录项启动的桌面应用通常不会继承当前终端的临时环境变量,因此临时方式无法可靠生效。

macOS

在可信终端中运行下面的完整命令:

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 提供方和专属模型目录;
  • 生成 ~/.codex/lumoswitch-model-catalog.json,让模型选择器只显示配置完成页给出的下游模型;
  • 先用 ChatGPT 自带的 Codex 校验模型目录,目录格式不受当前客户端支持时不会写入配置;
  • 安装 Lumoswitch 生成的能力感知模型目录,只开放模型与实际路由共同支持的推理档位;
  • 应用跨模型最低兼容配置:设置顶层 web_search = "disabled",并在已有或新建的 [features] 中设置 multi_agent = false;这会关闭 Codex 多智能体 namespace 与原生网页搜索,避免不识别这些工具的下游模型拒绝整个 Responses 请求,普通函数工具、终端命令和文件操作仍可使用;
  • 保留 MCP、权限、插件和其他提供方配置;
  • 将 Access Key 保存到权限为 0600~/.codex/.env,不把 Key 写进 TOML;
  • 备份 .env 时保留其他变量,但主动排除 LUMOSWITCH_API_KEY,不会把凭证复制进备份;
  • 在配置文件是符号链接、已有其他自定义模型目录,或存在无法安全识别的 TOML 写法时停止,不会静默覆盖原配置;
  • 完成后重新打开 ChatGPT。

Windows 原生 PowerShell 导入

当 ChatGPT 使用默认的 Windows 原生 Agent 环境时,它会读取 %USERPROFILE%\.codex 中的用户级 Codex 配置,并需要使用原生 PowerShell。如果 设置 > Agent 环境 已切换为 WSL,请先改回 Windows 原生 并重启 ChatGPT,因为 WSL 默认使用另一套 Codex 主目录。完全退出 ChatGPT 后,在可信的 PowerShell 终端中运行以下完整命令。开始菜单应用无法可靠继承当前 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. 日常使用

macOS 中从 Dock、Finder 或启动台,Windows 中从开始菜单正常打开 ChatGPT,再进入 Codex 即可。macOS 也可以运行:

open -a ChatGPT

不需要运行 codex。当 Lumoswitch 地址、Access Key 或下游模型名称改变时,替换占位符后重新运行上面的永久使用命令,再完全重启 ChatGPT。

新任务使用生成目录中的有效默认档位。/reasoning 只列出经过校验的档位;DeepSeek 这类开关式推理模型只显示一个等效的开启档位。

5. Codex IDE 与 CLI 的影响范围

ChatGPT 桌面端、Codex IDE 扩展和普通 Codex CLI 都会读取用户级 ~/.codex

使用入口是否读取本页配置启动方式
ChatGPT 中的 Codex正常打开 ChatGPT,再进入 Codex
Codex IDE 扩展修改配置后完全重启 IDE,再新建任务
普通 Codex CLI安装 CLI 后运行 codex
普通 ChatGPT 对话仍使用 ChatGPT 原来的服务

因此,不需要为 Codex IDE 扩展再维护另一份相同配置。如果以后安装 CLI,它也会读取这里的默认提供方和模型。需要隔离 CLI 配置时,请使用 Codex CLI 原生接入。需要有意识地让桌面端、CLI 和 IDE 共用时,请查看 ChatGPT 桌面端 + Codex CLI

6. 清除接入

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,它会同时移除顶层 modelmodel_provider 和旧版接入留下的 model_reasoning_effort;只有 web_search 仍精确等于 "disabled"[features] 中的 multi_agent 仍精确等于 false 时才移除这两个兼容值,用户后来改过的值和其他 feature 项都会保留,空的 [features] 表也可以保留。只有顶层 model_catalog_json 精确指向 ~/.codex/lumoswitch-model-catalog.json 时,才会移除该键并删除这个普通文件;其他自定义模型目录不会被碰触。遇到符号链接时不会跟随,而是停止并提示手动检查。

自动清除无法知道接入前被覆盖的默认模型和提供方是什么。若必须精确恢复,并且确认接入后没有其他配置改动,请选择安装命令生成的同一 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 重新打开。

7. 验证连接

在 ChatGPT 桌面端进入 Codex。模型选择器应只显示配置完成页给出的下游模型;选中它,新建任务并发送一个简单请求,再检查 Lumoswitch 请求日志中的模型名称是否完全一致。

如果没有响应,请检查:

  • 打开的是 Codex 任务,而不是普通 ChatGPT 对话;
  • 模型选择器显示的是下游模型名称,而不是 GPT-*
  • 配置 API 已启用 Responses 输出;
  • API 地址以 /v1 结尾,但不要再加 /responses
  • 下游模型名称与配置完成页完全一致;
  • 写入配置后已经完全退出并重新打开 ChatGPT。
自动配置停止时手动处理

在 ChatGPT 中打开 Settings > Configuration > Open config.toml,将下面内容合并到用户级 ~/.codex/config.toml。已有同名顶层键、[features] 或提供方表时修改原值,不要重复声明。该兼容配置会关闭 Codex 多智能体 namespace 与原生网页搜索,但普通函数工具、终端命令和文件操作仍可使用:

model = "your-client-facing-model-name"
model_provider = "lumoswitch"
model_catalog_json = "/Users/your-name/.codex/lumoswitch-model-catalog.json"
web_search = "disabled"

[features]
multi_agent = false

[model_providers.lumoswitch]
name = "Lumoswitch"
base_url = "https://api.lumoswitch.com/v1"
env_key = "LUMOSWITCH_API_KEY"
wire_api = "responses"
http_headers = { "X-Lumoswitch-Agent" = "codex-v1" }
requires_openai_auth = false

同时创建 ~/.codex/lumoswitch-model-catalog.json,将两处模型名称改成配置完成页给出的下游模型:

{
  "models": [
    {
      "slug": "your-client-facing-model-name",
      "display_name": "your-client-facing-model-name",
      "description": "Lumoswitch downstream model",
      "default_reasoning_level": "none",
      "supported_reasoning_levels": [],
      "supports_reasoning_summary_parameter": false,
      "default_reasoning_summary": "none",
      "shell_type": "shell_command",
      "visibility": "list",
      "supported_in_api": true,
      "priority": 0,
      "base_instructions": "You are a coding agent. Follow the user's instructions and use the available tools when needed.",
      "support_verbosity": false,
      "truncation_policy": { "mode": "tokens", "limit": 10000 },
      "supports_parallel_tool_calls": false,
      "experimental_supported_tools": [],
      "input_modalities": ["text"]
    }
  ]
}

model_catalog_json 必须是实际绝对路径,不能直接写 ~。如果配置中已经指向另一个自定义目录,不要覆盖它;请把上述模型条目合并进现有 JSON,并保留原路径。

然后在 ~/.codex/.env 中保存:

LUMOSWITCH_API_KEY="your-lumoswitch-access-key"

不要删除配置文件中的其他内容,也不要把 .env 提交到 Git。修改后完全退出并重新打开 ChatGPT。

发布维护字段
  • 平台 ID:chatgpt-desktop
  • 展示名称:ChatGPT Desktop (Codex)
  • 下游协议:responses
  • macOS/Linux 临时模式:关闭;commandTemplate 留空
  • macOS 永久策略:native
  • persistentCommandTemplate:复制本页 macOS「永久使用」代码框的完整内容
  • macOS futureCommandopen -a ChatGPT
  • Windows 临时模式:关闭;windowsCommandTemplate 留空
  • Windows 永久策略:native
  • windowsPersistentCommandTemplate:复制本页「Windows 原生 PowerShell 导入」代码框的完整内容
  • Windows windowsFutureCommandnull,从开始菜单重新打开
  • 展示顺序:15
  • 发布限制:macOS 与 Windows 模板只发布到各自标记的操作系统
  • 核验状态:官方配置契约已于 2026-08-27 重新核验;最近一次使用 ChatGPT 内置 Codex 验证专属目录只返回指定模型是在 2026-08-03;Windows PowerShell 已有结构与双语模板契约测试,真实 Windows Lumoswitch 流量仍待复核

常见问题

  • 终端提示 codex: command not found:此方式不依赖 CLI,可以忽略;不要为了使用桌面端而额外安装 CLI。
  • 普通 ChatGPT 对话没有使用 Lumoswitch:这是正常行为,请进入 Codex 工作区测试。
  • 模型选择器仍只有 GPT 模型:完全退出 ChatGPT,重新运行永久命令并确认没有“其他自定义模型目录”提示,再重新打开并新建任务。
  • 仍提示 The inference request is invalid:先确认模型名称和 Responses 输出,再重新执行一键导入刷新能力目录;仍失败时使用对应的 Lumoswitch 请求日志定位被拒绝的字段。
  • 需要恢复原配置:使用命令输出的备份后缀,把对应 .backup.<后缀> 文件复制回原路径。

参考:ChatGPT Windows 桌面端Codex 自定义模型提供方Codex 模型目录桌面端与 IDE 环境变量

On this page