查看: 4440|回复: 0

修复 CCSwitch + CPA 在CODEX上配置的gpt模型,思考过程、图片、fast模式等都不支持

[复制链接]

35

主题

5

回帖

454

积分

管理员

积分
454
发表于 2026-7-30 15:05:04 | 显示全部楼层 |阅读模式
[md]最近在折腾 Codex 对接 CPA 的账号池,链路大概是:

```plaintext
Codex → CC Switch → CPA 自定义 Responses 地址 → Codex 模型
```

CPA 里的模型是我自己在 CC Switch 里手动添加的。目前添加模型时能填写的内容比较少,基本只有:

```plaintext
模型 ID
上下文大小
```

后来我看了一下 CC Switch 生成的:

```plaintext
%USERPROFILE%\.codex\cc-switch-model-catalog.json
```

发现它和 Codex CLI 自己带的模型目录差别还挺大,让 AI 分析了一下。
官方目录可以用下面的命令查看:

```
codex debug models --bundled
```

## 发现的几个差异

CC Switch 生成的模型基本是套一个通用模板,而官方模型目录里有更多模型自己的配置。

比较明显的有:

* `base_instructions`:CC Switch 里只有一小段通用 Prompt,官方模型里的内容长很多;
* `model_messages`:官方目录里有完整的指令模板和 personality 配置,CC Switch 生成的目录里没有;
* `supported_reasoning_levels`:CC Switch 原来所有模型都是统一的 `none / high`,官方不同模型支持的等级并不一样;
* `input_modalities`:支持图片的官方模型是 `text,image`,CC Switch 原来通常只有 `text`;
* `supports_image_detail_original`:官方部分模型为 `true`,CC Switch 原来是 `false`;
* `supports_parallel_tool_calls`、`support_verbosity`、`default_verbosity` 等工具和输出相关字段也有差异;
* `truncation_policy`、`service_tiers`、`additional_speed_tiers` 等字段也不是完全一致。

这个问题不知道论坛里面有没有其他人发现,我的使用方法不对?CCswitch 是不是没考虑覆盖这种场景。

## 临时让 AI 写了个脚本合并配置文件

思路比较简单:

1. 读取 `codex debug models --bundled` 的官方目录;
2. 从官方目录动态找出 `gpt-*` 模型;
3. 用自定义模型的 `slug` 去包含匹配官方模型 ID;
4. 匹配到以后,用官方配置覆盖当前条目;
5. 但保留自定义模型原来的 `slug`,避免影响 CPA 路由;
6. 修改前自动备份,并支持只验证和预演。

比如:

```plaintext
自定义 slug:<provider-prefix>/gpt-5.6-luna
官方 slug:gpt-5.6-luna
```

脚本会使用官方 `gpt-5.6-luna` 的配置,但最后仍然保留自定义的 slug。

## 脚本

```
[CmdletBinding()]
param(
    [Parameter()]
    [string]$CatalogPath = (Join-Path $env:USERPROFILE '.codex\cc-switch-model-catalog.json'),

    [Parameter()]
    [string]$CodexCommand = 'codex.cmd',

    [Parameter()]
    [switch]$ValidateOnly,

    [Parameter()]
    [switch]$WhatIf
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Read-JsonFile {
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "Catalog file not found: $Path"
    }

    $text = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
    if ([string]::IsNullOrWhiteSpace($text)) {
        throw "Catalog file is empty: $Path"
    }

    return ($text | ConvertFrom-Json)
}

function Get-OfficialCatalog {
    param(
        [Parameter(Mandatory)]
        [string]$Command
    )

    $commandInfo = Get-Command $Command -ErrorAction Stop | Select-Object -First 1
    $commandPath = $commandInfo.Source
    if ([string]::IsNullOrWhiteSpace($commandPath)) {
        $commandPath = $commandInfo.Definition
    }

    if ([string]::IsNullOrWhiteSpace($commandPath)) {
        throw "Unable to resolve Codex command: $Command"
    }

    $utf8 = [System.Text.UTF8Encoding]::new($false)
    $startInfo = New-Object System.Diagnostics.ProcessStartInfo
    $startInfo.UseShellExecute = $false
    $startInfo.CreateNoWindow = $true
    $startInfo.RedirectStandardOutput = $true
    $startInfo.RedirectStandardError = $true
    $startInfo.StandardOutputEncoding = $utf8
    $startInfo.StandardErrorEncoding = $utf8

    # ProcessStartInfo cannot execute .cmd/.bat files directly when shell
    # execution is disabled, so invoke those through cmd.exe. The encoding is
    # configured on the redirected streams before the child process starts.
    if ($commandPath -match '\.(cmd|bat)$') {
        $startInfo.FileName = $env:ComSpec
        $startInfo.Arguments = '/d /s /c ""' + $commandPath + '" debug models --bundled"'
    }
    else {
        $startInfo.FileName = $commandPath
        $startInfo.Arguments = 'debug models --bundled'
    }

    $process = New-Object System.Diagnostics.Process
    $process.StartInfo = $startInfo
    try {
        if (-not $process.Start()) {
            throw "Failed to start Codex command: $commandPath"
        }

        # Read both streams asynchronously so a full stderr buffer cannot
        # deadlock the child process while stdout is being consumed.
        $stdoutTask = $process.StandardOutput.ReadToEndAsync()
        $stderrTask = $process.StandardError.ReadToEndAsync()
        $process.WaitForExit()

        $exitCode = $process.ExitCode
        $output = $stdoutTask.Result
        $stderr = $stderrTask.Result
    }
    finally {
        $process.Dispose()
    }

    if ($exitCode -ne 0) {
        throw "Failed to read the Codex bundled catalog. Exit code: $exitCode`n$stderr"
    }

    if ([string]::IsNullOrWhiteSpace($output)) {
        throw "Codex bundled catalog command returned empty stdout."
    }

    try {
        return ($output | ConvertFrom-Json)
    }
    catch {
        throw "Codex bundled catalog is not valid JSON: $($_.Exception.ToString())"
    }
}

function Copy-JsonValue {
    param(
        [Parameter(Mandatory)]
        [object]$Value
    )

    return ($Value | ConvertTo-Json -Depth 100 | ConvertFrom-Json)
}

function Find-OfficialModel {
    param(
        [Parameter(Mandatory)]
        [string]$CustomSlug,

        [Parameter(Mandatory)]
        [object[]]$OfficialModels
    )

    # Only use gpt-* entries and prefer longer, more specific slugs.
    $candidates = @($OfficialModels | Where-Object { $_.slug -like 'gpt-*' })
    $candidates = @($candidates | Sort-Object -Property @{ Expression = { $_.slug.Length }; Descending = $true })

    foreach ($officialModel in $candidates) {
        if ($CustomSlug.IndexOf(
                [string]$officialModel.slug,
                [System.StringComparison]::OrdinalIgnoreCase
            ) -ge 0) {
            return $officialModel
        }
    }

    return $null
}

function Get-MergedCatalog {
    param(
        [Parameter(Mandatory)]
        [object]$CustomCatalog,

        [Parameter(Mandatory)]
        [object]$OfficialCatalog
    )

    if ($null -eq $CustomCatalog.models -or $CustomCatalog.models.Count -eq 0) {
        throw 'Custom catalog has no models.'
    }

    if ($null -eq $OfficialCatalog.models -or $OfficialCatalog.models.Count -eq 0) {
        throw 'Official catalog has no models.'
    }

    $results = @()
    $report = @()

    foreach ($customModel in $CustomCatalog.models) {
        $customSlug = [string]$customModel.slug
        if ([string]::IsNullOrWhiteSpace($customSlug)) {
            throw 'A custom model is missing slug.'
        }

        $officialModel = Find-OfficialModel `
            -CustomSlug $customSlug `
            -OfficialModels @($OfficialCatalog.models)

        if ($null -eq $officialModel) {
            $results += $customModel
            $report += [pscustomobject]@{
                Model = $customSlug
                Status = 'Skipped'
                MatchedOfficialModel = $null
            }
            continue
        }

        # Use the complete official model configuration.
        $result = Copy-JsonValue -Value $officialModel

        # Match by the official slug but preserve the custom routing ID.
        $result.slug = $customSlug
        if ($customSlug -ne $officialModel.slug) {
            $result.display_name = $customSlug
        }

        $results += $result
        $report += [pscustomobject]@{
            Model = $customSlug
            Status = 'Merged'
            MatchedOfficialModel = [string]$officialModel.slug
        }
    }

    return [pscustomobject]@{
        Catalog = [pscustomobject]@{ models = @($results) }
        Report = @($report)
    }
}

function Write-CatalogAtomically {
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter(Mandatory)]
        [object]$Catalog
    )

    $backupPath = "$Path.bak-official-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
    $tempPath = "$Path.tmp"
    $json = $Catalog | ConvertTo-Json -Depth 100

    Copy-Item -LiteralPath $Path -Destination $backupPath -Force
    [System.IO.File]::WriteAllText(
        $tempPath,
        $json + [Environment]::NewLine,
        [System.Text.UTF8Encoding]::new($false)
    )

    # Parse the temporary file before replacing the original.
    Get-Content -LiteralPath $tempPath -Raw -Encoding UTF8 |
        ConvertFrom-Json |
        Out-Null

    Move-Item -LiteralPath $tempPath -Destination $Path -Force
    return $backupPath
}

$customCatalog = Read-JsonFile -Path $CatalogPath
$officialCatalog = Get-OfficialCatalog -Command $CodexCommand
$mergeResult = Get-MergedCatalog `
    -CustomCatalog $customCatalog `
    -OfficialCatalog $officialCatalog

$mergeResult.Report | Format-Table Model, Status, MatchedOfficialModel -AutoSize

$skipped = @($mergeResult.Report | Where-Object Status -eq 'Skipped')
if ($skipped.Count -gt 0) {
    Write-Warning "The following models did not match an official gpt-* entry and were kept unchanged:"
    $skipped | Format-Table Model -AutoSize
}

if ($ValidateOnly) {
    Write-Host "Validation completed without modifying: $CatalogPath"
    exit 0
}

if ($WhatIf) {
    Write-Host "WhatIf: would merge the Codex bundled catalog into: $CatalogPath"
    exit 0
}

$backupPath = Write-CatalogAtomically `
    -Path $CatalogPath `
    -Catalog $mergeResult.Catalog

Write-Host "Merge completed: $CatalogPath"
Write-Host "Backup: $backupPath"


```

## 使用方法

先只验证,不改文件:

```
.\tools\merge-codex-model-catalog.ps1 -ValidateOnly
```

确认匹配结果没问题后,可以预演一次:

```
.\tools\merge-codex-model-catalog.ps1 -WhatIf
```

最后再实际执行:

```
.\tools\merge-codex-model-catalog.ps1
```

脚本会自动备份原来的模型目录。如果 CPA 不支持官方目录里声明的某些工具或能力,可以用备份恢复,再单独调整相关字段。

效果,这是直接配置的 CPA 地址

[![image](https://cdn3.ldstatic.com/optimi ... 9b439_2_690x63.jpeg)image**1782×165 21.3 KB**](https://cdn3.ldstatic.com/origin ... 4f02c75d79b439.jpeg "image")

[![image](https://cdn3.ldstatic.com/optimi ... 9fcaa_2_690x158.png)image**1216×279 28.6 KB**](https://cdn3.ldstatic.com/origin ... 2e920ad55a9fcaa.png "image")
[/md]
潮流之家论坛免责声明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络收集整理,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。如果您喜欢该程序和内容,请支持正版,购买注册,得到更好的正版服务。我们非常重视版权问题,如有侵权请邮件与我们联系处理。敬请谅解!侵删请致信E-mail:10000@clzjwl.cn
qq776822619有问题找客服
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关注公众号
Archiver|小黑屋|潮流之家 劰载中...

相关侵权、举报、投诉及建议等,请发 E-mail:10000@clzjwl.cn

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.|晋ICP备2024047598号-3

在本版发帖
关注公众号
返回顶部