From 25dd95052e5e276e347ed03ed3e9384a76e18f38 Mon Sep 17 00:00:00 2001 From: Gaoshou101 <188431084+Gaoshou101@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:26:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?v0.2.12:=20count=5Ftokens=20=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=20404=E3=80=81=E6=80=9D=E8=80=83=E5=90=8E=E7=BC=80?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E4=B8=8E=20macOS=20runner=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - count_tokens 改为插件本地返回 404 count_tokens_unsupported,不再因 501 让 CPA v8.0.23 等宿主冷却凭证(CLIProxyAPI #7271)。 - 按 CPA 规则解析模型名思考后缀 model(level)/model(budget):旧宿主不再 因 404 冷却基础模型;v8.0.59 上原生 Messages 补回后缀等级,宿主注入的 reasoning_effort=auto 不再发往上游。 - 新增 cpa_count_tokens.py、cpa_thinking_suffix.py 与对应单元测试,接入 固定基线、四平台和最新 CPA 候选任务;候选任务加跑原生管理、第二轮协议 与热重载;新增每周定时复核。热升级前一版本改为 v0.2.11。 - macos-14 runner 已于 2026-11-02 停用,darwin-arm64 改用 macos-15, 以 MACOSX_DEPLOYMENT_TARGET=14.0 保持最低系统不变并用 otool 校验。 - 版本号、发布说明、部署文档与验证记录更新至 0.2.12。 --- .github/workflows/build.yml | 33 +++++++-- CHANGELOG.md | 14 ++++ README.md | 12 ++-- docs/DEPLOYMENT.md | 8 ++- docs/RELEASE_NOTES.md | 23 +++++- docs/VALIDATION.md | 27 +++++++ integrations/cpamp/README.md | 4 +- internal/bridge/count_tokens_test.go | 29 ++++++++ internal/bridge/executor.go | 39 ++++++++--- internal/bridge/service.go | 21 ++++-- internal/bridge/thinking_suffix.go | 93 +++++++++++++++++++++++++ internal/bridge/thinking_suffix_test.go | 73 +++++++++++++++++++ internal/bridge/types.go | 11 ++- registry.json | 2 +- tests/cpa_count_tokens.py | 59 ++++++++++++++++ tests/cpa_thinking_suffix.py | 76 ++++++++++++++++++++ 16 files changed, 495 insertions(+), 29 deletions(-) create mode 100644 internal/bridge/count_tokens_test.go create mode 100644 internal/bridge/thinking_suffix.go create mode 100644 internal/bridge/thinking_suffix_test.go create mode 100644 tests/cpa_count_tokens.py create mode 100644 tests/cpa_thinking_suffix.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 223afab..5bb14aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,9 @@ on: tags: ['v*'] pull_request: workflow_dispatch: + schedule: + # Weekly check against the newest CPA release; publish-release only runs for tags. + - cron: '23 3 * * 1' permissions: contents: read @@ -47,11 +50,13 @@ jobs: CPA_BINARY=/tmp/cpa-test/cli-proxy-api python3 tests/cpa_reliability.py CPA_BINARY=/tmp/cpa-test/cli-proxy-api python3 tests/cpa_diagnostics.py CPA_BINARY=/tmp/cpa-test/cli-proxy-api python3 tests/cpa_round2.py - gh release download v0.2.8 --repo Gaoshou101/CommandCodeBridge --pattern commandcodebridge_0.2.8_linux_amd64.zip --pattern checksums.txt --dir /tmp/bridge-previous + CPA_BINARY=/tmp/cpa-test/cli-proxy-api python3 tests/cpa_count_tokens.py + CPA_BINARY=/tmp/cpa-test/cli-proxy-api python3 tests/cpa_thinking_suffix.py + gh release download v0.2.11 --repo Gaoshou101/CommandCodeBridge --pattern commandcodebridge_0.2.11_linux_amd64.zip --pattern checksums.txt --dir /tmp/bridge-previous cd /tmp/bridge-previous sha256sum --check --ignore-missing checksums.txt cd "$GITHUB_WORKSPACE" - CPA_BINARY=/tmp/cpa-test/cli-proxy-api python3 tests/cpa_hot_reload.py --previous-archive /tmp/bridge-previous/commandcodebridge_0.2.8_linux_amd64.zip + CPA_BINARY=/tmp/cpa-test/cli-proxy-api python3 tests/cpa_hot_reload.py --previous-archive /tmp/bridge-previous/commandcodebridge_0.2.11_linux_amd64.zip - uses: actions/download-artifact@v4 with: name: cpamp-manager-test @@ -77,7 +82,8 @@ jobs: - runner: windows-2022 target: windows-amd64 extension: dll - - runner: macos-14 + # macos-14 was retired on 2026-11-02; MACOSX_DEPLOYMENT_TARGET below keeps the shipped minimum at 14.0. + - runner: macos-15 target: darwin-arm64 extension: dylib runs-on: ${{ matrix.runner }} @@ -101,9 +107,16 @@ jobs: env: CGO_ENABLED: '1' REPOSITORY: https://github.com/${{ github.repository }} + # Only read by the darwin toolchain; v0.2.11 and earlier shipped with minos 14.0. + MACOSX_DEPLOYMENT_TARGET: '14.0' run: | mkdir -p dist go build -trimpath -buildvcs=false -buildmode=c-shared -ldflags="-s -w -X commandcodebridge/internal/bridge.Repository=$REPOSITORY" -o dist/commandcodebridge.${{ matrix.extension }} ./cmd/commandcodebridge + if [ "${{ matrix.target }}" = darwin-arm64 ]; then + minos="$(otool -l dist/commandcodebridge.dylib | awk '/LC_BUILD_VERSION/ {found=1} found && $1 == "minos" {print $2; exit}')" + echo "darwin dylib minos: $minos" + if [ "$minos" != "$MACOSX_DEPLOYMENT_TARGET" ]; then echo "::error::dylib minos $minos != $MACOSX_DEPLOYMENT_TARGET"; exit 1; fi + fi helper=dist/commandcodebridge-login if [ "${{ matrix.target }}" = windows-amd64 ]; then helper="$helper.exe"; fi CGO_ENABLED=0 go build -trimpath -buildvcs=false -ldflags="-s -w" -o "$helper" ./cmd/commandcodebridge-login @@ -135,6 +148,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'Windows diagnostics integration failed' } python tests/cpa_round2.py if ($LASTEXITCODE -ne 0) { throw 'Windows round two integration failed' } + python tests/cpa_count_tokens.py + if ($LASTEXITCODE -ne 0) { throw 'Windows count_tokens integration failed' } + python tests/cpa_thinking_suffix.py + if ($LASTEXITCODE -ne 0) { throw 'Windows thinking suffix integration failed' } python tests/cpa_hot_reload.py if ($LASTEXITCODE -ne 0) { throw 'Windows live configuration integration failed' } - name: ARM64 native host integration @@ -164,6 +181,8 @@ jobs: python tests/cpa_e2e.py python tests/cpa_native_management.py python tests/cpa_reliability.py + python tests/cpa_count_tokens.py + python tests/cpa_thinking_suffix.py python tests/cpa_hot_reload.py - uses: actions/upload-artifact@v4 with: @@ -243,7 +262,8 @@ jobs: needs: cpamp-web runs-on: ubuntu-24.04 # Report candidate compatibility without changing the pinned release gate. - continue-on-error: true + # The weekly scheduled run fails visibly so a breaking CPA release is noticed. + continue-on-error: ${{ github.event_name != 'schedule' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 @@ -275,6 +295,11 @@ jobs: python tests/cpa_e2e.py 2>&1 | tee -a dist/upstream-compatibility/result.txt python tests/cpa_reliability.py 2>&1 | tee -a dist/upstream-compatibility/result.txt python tests/cpa_diagnostics.py 2>&1 | tee -a dist/upstream-compatibility/result.txt + python tests/cpa_native_management.py 2>&1 | tee -a dist/upstream-compatibility/result.txt + python tests/cpa_round2.py 2>&1 | tee -a dist/upstream-compatibility/result.txt + python tests/cpa_count_tokens.py 2>&1 | tee -a dist/upstream-compatibility/result.txt + python tests/cpa_thinking_suffix.py 2>&1 | tee -a dist/upstream-compatibility/result.txt + python tests/cpa_hot_reload.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - name: Record candidate result if: always() shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 182b1ac..c11d9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.2.12] - 2026-11-07 + +### Fixed + +- `count_tokens` 改为插件本地返回 404 `count_tokens_unsupported`,不访问上游。CPA v8.0.23 等较早宿主会把原先的 501 当作凭证故障并冷却,随后的 `/v1/messages` 返回 503 `auth_unavailable`(CLIProxyAPI #7271,上游修复 `1c53721`);已在 v8.0.23 复现并验证修复。 +- 支持 CPA 模型名思考后缀 `model(level)`/`model(budget)`。v8.0.59 之前的宿主把带后缀的模型名原样交给插件,插件此前返回 404,宿主随即把凭证标为 unavailable(v8.0.23 实测约 12 小时后才重试),未带后缀的同模型请求也变为 503。插件现按宿主规则解析后缀(`none`、`auto`/`-1`、`minimal`–`max`,数值预算按 512/1024/8192/24576 分级),并覆盖请求体中的推理设置;无法识别的后缀忽略。 +- CPA v8.0.59 会先剥离后缀,但插件以原始请求体重建原生 Messages 请求时丢失了后缀指定的等级,现已从原始模型名补回。宿主注入的 `reasoning_effort=auto` 不再发往上游,也不再在 Anthropic 模型上返回 400。 + +### Changed + +- 新增 `cpa_count_tokens.py` 与 `cpa_thinking_suffix.py` 宿主联测,接入固定 v8.0.23 基线、Windows、Linux/macOS ARM64 和最新 CPA 候选任务;候选任务另跑原生管理、第二轮协议和热重载检查。 +- 新增每周定时运行,用于复核最新 CPA 候选;定时运行时候选失败会使工作流显式失败,非标签运行不发布。热升级联测的前一版本改为 v0.2.11。 +- macOS Apple Silicon 包改在 `macos-15` runner 构建(GitHub 已于 2026-11-02 停用 `macos-14`),以 `MACOSX_DEPLOYMENT_TARGET=14.0` 保持最低 macOS 14.0 不变,并在构建后校验。 + ## [0.2.11] - 2026-10-23 ### Fixed diff --git a/README.md b/README.md index 54600fe..d001080 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,11 @@ - **原生额度窗口**:在 CPAMP 列表、卡片和详情查看 5h、周、月额度,查询窗口请求统计、预测和周期快照。 - **请求记录**:查看凭证健康度、tokens、缓存用量、预估花费、首字耗时、输出速率及模型统计;支持明暗主题和手机布局。 -当前版本 **v0.2.11**。插件调用官方 Provider API;Go 订阅的 `/alpha/generate` 路径和固定上游 provider 模式不在支持范围内。账户用量通过官方 `/alpha/...` 查询接口读取,模型权限以实际套餐为准。 +当前版本 **v0.2.12**。插件调用官方 Provider API;Go 订阅的 `/alpha/generate` 路径和固定上游 provider 模式不在支持范围内。账户用量通过官方 `/alpha/...` 查询接口读取,模型权限以实际套餐为准。 + +### v0.2.12 更新 + +修复两类会让 CPA 宿主误冷却凭证的请求:`count_tokens` 改为本地 404,不再在 v8.0.23 等宿主上触发随后的 503 `auth_unavailable`;`model(high)`、`model(16384)` 等思考后缀按宿主规则解析,不再因 404 冷却基础模型。CI 新增对应宿主联测,并每周复核最新 CPA 候选版本。 ### v0.2.11 更新 @@ -39,7 +43,7 @@ plugins: - https://raw.githubusercontent.com/Gaoshou101/CommandCodeBridge/main/registry.json ``` -保存并重载配置,在 CPAMP「插件商店」找到 **CommandCodeBridge**,选择 Latest 或 **v0.2.11** 安装。支持热升级的宿主会下载匹配平台的 ZIP、验证校验值并切换版本;安装后检查插件控制台显示的运行版本。 +保存并重载配置,在 CPAMP「插件商店」找到 **CommandCodeBridge**,选择 Latest 或 **v0.2.12** 安装。支持热升级的宿主会下载匹配平台的 ZIP、验证校验值并切换版本;安装后检查插件控制台显示的运行版本。 也可从 [Releases](https://github.com/Gaoshou101/CommandCodeBridge/releases/latest) 手动下载 ZIP 和 `checksums.txt`。按 **CPA 所在服务器**选择动态库: @@ -65,7 +69,7 @@ Linux 发布包要求 glibc ≥2.34;Alpine/musl 需自行构建。手动安装 ## 安装或更新原生面板 -**升级插件后还要更新 CPAMP 页面**,才能使用新版凭证布局。v0.2.11 动态库已内嵌匹配页面。 +**升级插件后还要更新 CPAMP 页面**,才能使用新版凭证布局。v0.2.12 动态库已内嵌匹配页面。 CPA 与 Manager Server 在同一主机直接运行时: @@ -80,7 +84,7 @@ CPA 与 Manager Server 在同一主机直接运行时: | 首次启用外部页面,或更改 Manager Server 的页面路径 | 重启 Manager Server 一次 | | 迁移插件数据目录,或旧宿主不支持动态库热升级 | 安排相应服务重启 | -Manager Server 在另一台主机或独立容器中时,在其运行环境安装 `commandcodebridge_0.2.11_cpamp-web.zip` 内的 `management.html`,通过 `PANEL_PATH` 或 `panelPath` 托管。面板源码基线见 [compatibility.json](./internal/bridge/compatibility.json) 的 `cpamp_source_commit`;不同版本应在匹配源码上应用适配。 +Manager Server 在另一台主机或独立容器中时,在其运行环境安装 `commandcodebridge_0.2.12_cpamp-web.zip` 内的 `management.html`,通过 `PANEL_PATH` 或 `panelPath` 托管。面板源码基线见 [compatibility.json](./internal/bridge/compatibility.json) 的 `cpamp_source_commit`;不同版本应在匹配源码上应用适配。 **完整周期快照另需 Manager Server 接受 `commandcode` 平台。** 前端包附带适配脚本、服务器名单补丁和测试;按现有 Manager Server 的同一源码版本重建并部署。仅更新插件和页面不能替代这个服务器改动。[原生集成说明](./integrations/cpamp/README.md)包含构建、HTTP 生效核验和回滚步骤。 diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index adde7ad..9604681 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,6 +1,12 @@ # CPAMP 部署、升级与回滚 -本文说明 CommandCodeBridge v0.2.11 的安装、页面更新与回滚。路径应以实际运行环境为准;历史排查中的路径仅用于说明当时的问题。 +本文说明 CommandCodeBridge v0.2.12 的安装、页面更新与回滚。路径应以实际运行环境为准;历史排查中的路径仅用于说明当时的问题。 + +## v0.2.12:count_tokens 与思考后缀兼容 + +1. 更新插件至 v0.2.12,确认实际运行版本;配置无需修改。 +2. 页面功能未变,但页面带插件版本标识,仍按下节步骤在「设置 → CPAMP 原生面板」更新页面并强刷浏览器。 +3. 此前因 `count_tokens` 或带后缀模型名被冷却的凭证会显示 unavailable(v8.0.23 实测约 12 小时后重试);重启 CPA 可立即恢复。插件热升级是否清除宿主冷却未验证。 ## v0.2.11:更新 Antigravity 面板身份 diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index 12164a6..cbb22b9 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,6 +1,27 @@ # CommandCodeBridge 发布说明 -## v0.2.11 +## v0.2.12 + +发布日期:2026-11-07。CPA 原生插件,C ABI 1 / RPC schema 6;调用官方 Provider API。 + +### 本次更新 + +- **count_tokens 不再导致凭证冷却**:插件本地返回 404 `count_tokens_unsupported`,不访问上游。较早宿主(含 v8.0.23)会因原先的 501 冷却凭证,使随后的 `/v1/messages` 返回 503 `auth_unavailable`(CLIProxyAPI #7271)。 +- **支持模型名思考后缀**:`model(high)`、`model(16384)` 等写法在 v8.0.23 起的宿主上均可用,按宿主规则换算推理等级并覆盖请求体设置;修复前较早宿主会冷却对应基础模型。v8.0.59 上原生 Messages 丢失后缀等级、`auto` 等级在 Anthropic 模型返回 400 的问题一并修复。 +- **兼容性回归**:新增 count_tokens 与思考后缀宿主联测,覆盖固定基线、四平台和最新 CPA 候选;每周定时复核最新 CPA,候选失败会显式报警且不发布。 +- **macOS 构建环境**:GitHub 已停用 `macos-14` runner,macOS 包改在 `macos-15` 构建;最低系统要求仍为 macOS 14.0。 + +### 升级步骤 + +1. 更新插件至 **v0.2.12**,确认实际运行版本。无需修改配置。 +2. 本次未改变 CPAMP 页面功能;页面带插件版本标识,仍建议在「设置 → CPAMP 原生面板」更新页面,异机/独立容器使用 `commandcodebridge_0.2.12_cpamp-web.zip`。 +3. 使用思考后缀的客户端无需改动。在 v8.0.23 上实测,修复前一次带后缀请求会使凭证显示 unavailable,约 12 小时后才重试;重启 CPA 后立即恢复。插件热升级本身是否清除宿主已记录的冷却未验证,如仍不可用请重启 CPA。 + +### 下载与校验 + +四平台原生 ZIP、独立 CPAMP 前端 ZIP、五个 `.zip.sha256` 和统一 `checksums.txt`,共 **11 个资产**。CPA 发布门槛仍为 v8.0.23;v8.0.59 作为候选版本在 Windows 本地完成全套宿主联测,并由 CI 候选任务持续复核,不改变发布门槛。本次只使用本地模拟上游,未进行真实模型生成,未访问或修改任何部署服务器。 + +## 已发布 v0.2.11 发布日期:2026-10-23。CPA 原生插件,C ABI 1 / RPC schema 6;调用官方 Provider API。 diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index a01f76c..a35a417 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -1,5 +1,32 @@ # 开发验证记录 +## v0.2.12 CPA 兼容修复(2026-11-07,未发布源码) + +本轮只使用本地模拟上游,未进行真实模型生成,未访问或修改任何部署服务器。CPA 发布门槛仍为 v8.0.23;`compatibility.json` 未改动。 + +### 问题复现 + +- **count_tokens**:v0.2.11 对 `/v1/messages/count_tokens` 返回 501。在 Windows CPA v8.0.23 上复现:宿主把 501 当作凭证故障冷却凭证,随后同模型的 `/v1/messages` 返回 503 `auth_unavailable`(CLIProxyAPI #7271,上游修复 `1c53721`)。 +- **思考后缀**:v8.0.23 把 `mock-chat(high)` 原样交给插件,v0.2.11 返回 404,宿主随即把凭证标为 unavailable,未带后缀的同模型请求也变为 503。实测约 12 小时后宿主才重试;重启 CPA 立即恢复。插件热升级本身是否清除宿主已记录的冷却未验证。 +- v8.0.59(上游 `c9af529`)由宿主剥离后缀,但 v0.2.11 以原始请求体重建原生 Messages 请求时丢失后缀等级;宿主注入的 `reasoning_effort=auto` 在 Chat 模型上原样发往上游,在 Anthropic 模型上返回 400 `unsupported Anthropic reasoning effort`。 +- 新增 `tests/cpa_thinking_suffix.py` 使用修复前动态库在 v8.0.23 与 v8.0.59 上均失败,确认该测试能识别上述问题。 + +### 自动化与宿主联测 + +- Windows Go 1.26 `go vet ./internal/... ./cmd/...`、`gofmt -l` 与 `go test ./internal/... ./cmd/... -count=1` 通过,包括新增的 count_tokens 与思考后缀单元测试;Node **31 项**通过;`test_cpamp_panel_validation.py` 在 WSL Python 3.10 下 6 项通过(Windows 商店版 Python 的 `python.exe` 无法被 `shutil.copyfile` 读取,该用例在本机 Windows 上属于环境错误,非代码失败)。 +- Windows amd64 动态库(WSL 交叉编译,晚于最后一次源码修改)在实际 Windows CPA **v8.0.23** 与 **v8.0.59** 上各运行 9 项,共 **18/18 通过**:`cpa_e2e.py`(25 个请求)、`cpa_native_management.py`、`cpa_reliability.py`、`cpa_diagnostics.py`、`cpa_round2.py`(13 次模拟请求,真实请求 0)、`cpa_count_tokens.py`(两类模型各 3 轮 count_tokens 均为 404、随后 Messages 均为 200)、`cpa_thinking_suffix.py`(Chat/Responses/原生 Messages、流式与请求体覆盖,事后无冷却)、`cpa_hot_reload.py`,以及以 v0.2.11 正式 Windows 包(SHA-256 与发布 checksums 一致)为前一版本的 `cpa_hot_reload.py --previous-archive`,确认同一 CPA 进程内 0.2.11 → 0.2.12 热升级。 +- `tests/cpa_store_install.py` 在隔离 Windows CPA v8.0.59 中通过:匿名公开源发现、Release 选包、校验安装、重启加载。当时 `main` 的 `registry.json` 仍为 0.2.11,因此该次按 0.2.11 断言,验证的是 v8.0.59 宿主的商店安装路径,不代表 v0.2.12 资产已发布。 + +### CI 变更 + +- GitHub 已于 2026-11-02 停用 `macos-14` runner;PR #14 的 macOS 任务即因 `The configuration 'macos-14-arm64-us-default' is not supported` 被取消。darwin-arm64 改用 `macos-15`,并以 `MACOSX_DEPLOYMENT_TARGET=14.0` 保持与 v0.2.11 相同的最低系统(v0.2.11 发布包 dylib 的 `LC_BUILD_VERSION` 为 minos 14.0/SDK 14.5),构建后用 `otool -l` 校验。macOS 构建只能在 CI 验证,本机未运行。 +- 新增每周定时运行;最新 CPA 候选任务加跑原生管理、第二轮协议、count_tokens、思考后缀与热重载,定时运行时候选失败会使工作流失败。热升级联测的前一版本改为 v0.2.11。 + +### 待验证 + +- 四平台 CI(尤其 `macos-15` 与 Linux ARM64 上的新增联测)。 +- 真实上游下的思考后缀等级效果与 count_tokens 客户端行为。 + ## v0.2.10 发版记录(2026-10-18) 用户已授权合并 PR #12 并发版。发布任务新增标签与源码版本、同一来源提交、四平台及 CPAMP 页面一致性检查;仅在协议测试、构建和校验通过后发布 11 个资产。实际发布状态以 [Release](https://github.com/Gaoshou101/CommandCodeBridge/releases/tag/v0.2.10) 和对应标签 CI 为准。下方“未发布”描述保留为当轮验证时的历史状态,不代表真实上游验收已经完成。本次不部署目标服务。 diff --git a/integrations/cpamp/README.md b/integrations/cpamp/README.md index 647540d..7b3af0e 100644 --- a/integrations/cpamp/README.md +++ b/integrations/cpamp/README.md @@ -39,8 +39,8 @@ CPAMP 当前源码的额度平台列表固定为内置平台,因此仅增加 ## 安装 1. 备份原插件动态库、CPA auth-dir、插件 data_dir 以及 CPAMP 当前管理页面。 -2. 通过支持热升级的 CPA 插件商店更新 CommandCodeBridge;确认运行版本为 `0.2.11`。从 [v0.2.11 Release](https://github.com/Gaoshou101/CommandCodeBridge/releases/tag/v0.2.11) 选择匹配平台 ZIP,并核验 `checksums.txt`。 -3. **同时升级管理页面**。同机使用上节一键入口;异机面板匹配包内 `BUILD.json` 的 CPAMP 基线时,下载 Release 的 `commandcodebridge_0.2.11_cpamp-web.zip`,使用包内 `management.html`,按下节安装。需要自行构建时,在对应 CPAMP 源码目录应用补丁: +2. 通过支持热升级的 CPA 插件商店更新 CommandCodeBridge;确认运行版本为 `0.2.12`。从 [v0.2.12 Release](https://github.com/Gaoshou101/CommandCodeBridge/releases/tag/v0.2.12) 选择匹配平台 ZIP,并核验 `checksums.txt`。 +3. **同时升级管理页面**。同机使用上节一键入口;异机面板匹配包内 `BUILD.json` 的 CPAMP 基线时,下载 Release 的 `commandcodebridge_0.2.12_cpamp-web.zip`,使用包内 `management.html`,按下节安装。需要自行构建时,在对应 CPAMP 源码目录应用补丁: ```bash python /path/to/CommandCodeBridge/integrations/cpamp/apply.py /path/to/CPA-Manager-Plus --check diff --git a/internal/bridge/count_tokens_test.go b/internal/bridge/count_tokens_test.go new file mode 100644 index 0000000..1def8ce --- /dev/null +++ b/internal/bridge/count_tokens_test.go @@ -0,0 +1,29 @@ +package bridge + +import ( + "encoding/json" + "errors" + "testing" +) + +// Token counting must stay an availability-neutral endpoint gap: CPA hosts +// before CLIProxyAPI 1c53721 cool a credential down after a count_tokens 501. +func TestCountTokensReportsAvailabilityNeutralGap(t *testing.T) { + s := NewService() + called := false + s.SetHost(func(string, any, any) error { called = true; return nil }) + _, err := s.Handle("executor.count_tokens", json.RawMessage(`{"Model":"mock-claude","Payload":"e30="}`)) + var api *APIError + if !errors.As(err, &api) { + t.Fatalf("count_tokens error = %#v, want *APIError", err) + } + if api.Status != 404 || api.Code() != "count_tokens_unsupported" || !api.Local { + t.Fatalf("count_tokens error = %+v, want local 404 count_tokens_unsupported", api) + } + if api.Status == 501 || api.Status == 429 || api.Status == 401 || api.Status == 403 { + t.Fatalf("count_tokens status %d would change credential availability", api.Status) + } + if called { + t.Fatal("count_tokens must not call the host or upstream") + } +} diff --git a/internal/bridge/executor.go b/internal/bridge/executor.go index 58e7858..7466473 100644 --- a/internal/bridge/executor.go +++ b/internal/bridge/executor.go @@ -239,27 +239,33 @@ func (s *Service) acquire(c Credential, model string) (func(error), error) { } }, nil } -func (s *Service) prepare(r ExecutorRequest) (map[string]any, Credential, Model, error) { + +// prepare also returns the reasoning effort selected by a CPA model-name +// thinking suffix, or "" when the request has no recognized suffix. +func (s *Service) prepare(r ExecutorRequest) (map[string]any, Credential, Model, string, error) { c, e := s.credential(r) if e != nil { - return nil, c, Model{}, e + return nil, c, Model{}, "", e } if c.ProxyURL != "" { - return nil, c, Model{}, fail(400, "this CPA HTTP bridge supports the global proxy only; remove the credential proxy_url and configure the CPA global proxy") + return nil, c, Model{}, "", fail(400, "this CPA HTTP bridge supports the global proxy only; remove the credential proxy_url and configure the CPA global proxy") } - m, e := s.model(r.Model) + m, suffix, e := s.model(r.Model) if e != nil { - return nil, c, m, e + return nil, c, m, "", e } body, e := decodeObject(r.Payload) if e != nil { - return nil, c, m, e + return nil, c, m, "", e } if len(list(body["messages"])) == 0 { - return nil, c, m, fail(400, "messages must be a nonempty array") + return nil, c, m, "", fail(400, "messages must be a nonempty array") } body["model"] = m.UpstreamID - return body, c, m, nil + if suffix == "" { + suffix = originalThinkingSuffix(r.OriginalRequest, m.ID) + } + return body, c, m, applyThinkingSuffix(body, suffix), nil } func (s *Service) execute(r ExecutorRequest, stream bool) (any, error) { if e := s.begin(); e != nil { @@ -267,7 +273,7 @@ func (s *Service) execute(r ExecutorRequest, stream bool) (any, error) { } r.Stream = stream start := time.Now() - body, c, m, e := s.prepare(r) + body, c, m, suffixEffort, e := s.prepare(r) executionConfig := s.config() entry := s.newLog(r, c, m, start) entry.CredentialScope = credentialEvidenceScope(c, executionConfig) @@ -312,7 +318,20 @@ func (s *Service) execute(r ExecutorRequest, stream bool) (any, error) { original, err := decodeObject(r.OriginalRequest) if err == nil && len(list(original["messages"])) > 0 { original["model"] = m.UpstreamID - if m.Reasoning.Enabled && entry.ReasoningFrom != entry.ReasoningTo { + if suffixEffort != "" { + // The model-name suffix overrides reasoning in the body. + delete(original, "reasoning_effort") + delete(original, "thinking") + if output := object(original["output_config"]); output != nil { + delete(output, "effort") + if len(output) == 0 { + delete(original, "output_config") + } + } + if effort := body["reasoning_effort"]; effort != nil { + original["reasoning_effort"] = effort + } + } else if m.Reasoning.Enabled && entry.ReasoningFrom != entry.ReasoningTo { if body["reasoning_effort"] != nil { original["reasoning_effort"] = body["reasoning_effort"] } else { diff --git a/internal/bridge/service.go b/internal/bridge/service.go index a53b8b1..44c0a92 100644 --- a/internal/bridge/service.go +++ b/internal/bridge/service.go @@ -232,7 +232,7 @@ func (s *Service) Handle(method string, raw json.RawMessage) (any, error) { } return s.execute(r, method == "executor.execute_stream") case "executor.count_tokens": - return nil, fail(501, "CommandCode has no exact token counting endpoint") + return nil, errCountTokensUnsupported() case "executor.http_request": return nil, fail(400, "use the model executor") case "management.register": @@ -281,13 +281,24 @@ func (s *Service) modelRegistration() any { } return map[string]any{"Provider": Provider, "Models": models} } -func (s *Service) model(alias string) (Model, error) { - for _, m := range s.config().Models { + +// model resolves a configured alias. When only the alias without a CPA +// thinking suffix exists, it also returns the raw suffix for the caller. +func (s *Service) model(alias string) (Model, string, error) { + models := s.config().Models + for _, m := range models { if m.ID == alias { - return m, nil + return m, "", nil + } + } + if base, raw, ok := splitThinkingSuffix(alias); ok { + for _, m := range models { + if m.ID == base { + return m, raw, nil + } } } - return Model{}, fail(404, "model alias is not configured") + return Model{}, "", fail(404, "model alias is not configured") } func authData(c Credential, name string) any { if name == "" { diff --git a/internal/bridge/thinking_suffix.go b/internal/bridge/thinking_suffix.go new file mode 100644 index 0000000..14a4c88 --- /dev/null +++ b/internal/bridge/thinking_suffix.go @@ -0,0 +1,93 @@ +package bridge + +import ( + "encoding/json" + "strconv" + "strings" +) + +// CPA lets clients append a thinking level or budget to a model name, such as +// "gpt-5(high)" or "claude-sonnet(16384)". Since CLIProxyAPI c9af529 (first +// released in v8.0.59) the host strips that suffix and injects the matching +// reasoning settings before it calls a plugin. Older hosts, including the +// pinned v8.0.23 baseline, pass the suffixed name through unchanged. A 404 +// model_not_found for that name made those hosts cool down the base model for +// the credential, so later unsuffixed requests failed with auth_unavailable. +// The bridge therefore resolves the suffix itself with the host's rules. + +// splitThinkingSuffix mirrors CPA thinking.ParseSuffix. +func splitThinkingSuffix(model string) (base, raw string, ok bool) { + open := strings.LastIndex(model, "(") + if open < 0 || !strings.HasSuffix(model, ")") { + return model, "", false + } + return strings.TrimSpace(model[:open]), model[open+1 : len(model)-1], true +} + +// thinkingSuffixEffort converts a raw suffix to a Chat reasoning_effort using +// CPA's special, level and budget-to-level rules. "unset" requests automatic +// thinking, which leaves the upstream default in place. Unknown suffixes are +// ignored, matching hosts that strip the suffix before the plugin call. +func thinkingSuffixEffort(raw string) (string, bool) { + switch lower := strings.ToLower(raw); lower { + case "none": + return "none", true + case "auto", "-1": + return "unset", true + case "minimal", "low", "medium", "high", "xhigh", "max": + return lower, true + } + budget, err := strconv.Atoi(raw) + switch { + case err != nil || budget < 0: + return "", false + case budget == 0: + return "none", true + case budget <= 512: + return "minimal", true + case budget <= 1024: + return "low", true + case budget <= 8192: + return "medium", true + case budget <= 24576: + return "high", true + default: + return "xhigh", true + } +} + +// originalThinkingSuffix recovers the client's suffix when a newer host has +// already removed it from the executor model. Native Messages requests are +// rebuilt from the original body, so the suffix must be reapplied there. +func originalThinkingSuffix(original []byte, alias string) string { + if len(original) == 0 { + return "" + } + var request struct { + Model string `json:"model"` + } + if json.Unmarshal(original, &request) != nil { + return "" + } + if base, raw, ok := splitThinkingSuffix(request.Model); ok && base == alias { + return raw + } + return "" +} + +// applyThinkingSuffix lets a recognized suffix override body reasoning, as the +// host pipeline does. CPA's internal "auto" level is not a Chat +// reasoning_effort value, so it is removed instead of being sent upstream. +func applyThinkingSuffix(body map[string]any, raw string) string { + effort, ok := thinkingSuffixEffort(raw) + if ok { + body["reasoning_effort"] = effort + } + if value := str(body["reasoning_effort"]); value == "unset" || value == "auto" { + delete(body, "reasoning_effort") + } + if !ok { + return "" + } + return effort +} diff --git a/internal/bridge/thinking_suffix_test.go b/internal/bridge/thinking_suffix_test.go new file mode 100644 index 0000000..1762d63 --- /dev/null +++ b/internal/bridge/thinking_suffix_test.go @@ -0,0 +1,73 @@ +package bridge + +import "testing" + +func TestThinkingSuffixMatchesHostRules(t *testing.T) { + cases := map[string]string{ + "none": "none", "NONE": "none", "auto": "unset", "-1": "unset", + "minimal": "minimal", "low": "low", "medium": "medium", "HIGH": "high", "xhigh": "xhigh", "max": "max", + "0": "none", "1": "minimal", "512": "minimal", "513": "low", "1024": "low", + "1025": "medium", "8192": "medium", "8193": "high", "24576": "high", "24577": "xhigh", + } + for raw, want := range cases { + if got, ok := thinkingSuffixEffort(raw); !ok || got != want { + t.Errorf("thinkingSuffixEffort(%q) = %q, %v; want %q", raw, got, ok, want) + } + } + for _, raw := range []string{"", "ultra", " high", "-2", "bogus"} { + if got, ok := thinkingSuffixEffort(raw); ok { + t.Errorf("thinkingSuffixEffort(%q) = %q; unknown suffixes must be ignored", raw, got) + } + } +} + +// Hosts before CLIProxyAPI v8.0.59 pass "alias(level)" to the plugin. A 404 +// there cools the base alias down for the credential. +func TestModelResolvesThinkingSuffixToBaseAlias(t *testing.T) { + s := NewService() + s.cfg.Models = []Model{{ID: "chat", UpstreamID: "upstream-chat"}, {ID: "odd(name)", UpstreamID: "upstream-odd"}} + for alias, want := range map[string][2]string{ + "chat": {"upstream-chat", ""}, + "chat(high)": {"upstream-chat", "high"}, + "chat (8192)": {"upstream-chat", "8192"}, + "chat(bogus)": {"upstream-chat", "bogus"}, + "odd(name)": {"upstream-odd", ""}, + "odd(name)(1)": {"upstream-odd", "1"}, + } { + m, suffix, err := s.model(alias) + if err != nil || m.UpstreamID != want[0] || suffix != want[1] { + t.Errorf("model(%q) = %q, %q, %v; want %q, %q", alias, m.UpstreamID, suffix, err, want[0], want[1]) + } + } + for _, alias := range []string{"missing", "missing(high)", "chat(high"} { + if _, _, err := s.model(alias); err == nil || statusOf(err) != 404 { + t.Errorf("model(%q) error = %v, want 404", alias, err) + } + } +} + +func TestThinkingSuffixOverridesBodyReasoning(t *testing.T) { + body := map[string]any{"reasoning_effort": "high"} + if got := applyThinkingSuffix(body, "low"); got != "low" || body["reasoning_effort"] != "low" { + t.Fatalf("suffix low: effort %q, body %v", got, body) + } + body = map[string]any{"reasoning_effort": "high"} + if got := applyThinkingSuffix(body, "auto"); got != "unset" || body["reasoning_effort"] != nil { + t.Fatalf("suffix auto: effort %q, body %v", got, body) + } + body = map[string]any{"reasoning_effort": "high"} + if got := applyThinkingSuffix(body, "ultra"); got != "" || body["reasoning_effort"] != "high" { + t.Fatalf("unknown suffix: effort %q, body %v", got, body) + } + // Newer hosts translate the "auto" suffix into reasoning_effort=auto. + body = map[string]any{"reasoning_effort": "auto"} + if got := applyThinkingSuffix(body, ""); got != "" || body["reasoning_effort"] != nil { + t.Fatalf("host auto: effort %q, body %v", got, body) + } + if got := originalThinkingSuffix([]byte(`{"model":"chat(xhigh)","messages":[]}`), "chat"); got != "xhigh" { + t.Fatalf("original suffix = %q", got) + } + if got := originalThinkingSuffix([]byte(`{"model":"other(xhigh)"}`), "chat"); got != "" { + t.Fatalf("original suffix for another alias = %q", got) + } +} diff --git a/internal/bridge/types.go b/internal/bridge/types.go index e2bfd29..fd81015 100644 --- a/internal/bridge/types.go +++ b/internal/bridge/types.go @@ -12,7 +12,7 @@ import ( "time" ) -const Version = "0.2.11" +const Version = "0.2.12" const PluginID = "commandcodebridge" const Provider = "commandcode" @@ -35,6 +35,15 @@ func fail(status int, message string) error { return &APIError{Status: status, Kind: "invalid_request", Message: message} } +// CommandCode has no exact token counting endpoint, and the plugin never +// fabricates a count. Report an endpoint-level gap rather than HTTP 501: CPA +// treats a count_tokens 404 as availability-neutral on every supported host, +// whereas hosts before CLIProxyAPI 1c53721 (#7271) cool the credential down +// after a 501, so the following /v1/messages request fails with 503. +func errCountTokensUnsupported() error { + return &APIError{Status: 404, Kind: "count_tokens_unsupported", Message: "count_tokens endpoint is not available for CommandCode", Local: true} +} + type HostCall func(string, any, any) error type ExecutorRequest struct { pluginTest bool diff --git a/registry.json b/registry.json index 8da3dd8..ab2e97a 100644 --- a/registry.json +++ b/registry.json @@ -6,7 +6,7 @@ "name": "CommandCodeBridge", "description": "CommandCode 官方 Provider API:多凭据、模型映射、套餐用量和请求统计。", "author": "CommandCodeBridge contributors", - "version": "0.2.11", + "version": "0.2.12", "repository": "https://github.com/Gaoshou101/CommandCodeBridge", "homepage": "https://github.com/Gaoshou101/CommandCodeBridge", "license": "MIT", diff --git a/tests/cpa_count_tokens.py b/tests/cpa_count_tokens.py new file mode 100644 index 0000000..bdab4fc --- /dev/null +++ b/tests/cpa_count_tokens.py @@ -0,0 +1,59 @@ +"""Unsupported token counting must not cool down CommandCode inference. + +CPA_BINARY=/path/to/cli-proxy-api BRIDGE_LIBRARY=/path/to/commandcodebridge.so python3 tests/cpa_count_tokens.py + +Claude-compatible clients call /v1/messages/count_tokens before many requests. +CommandCode has no exact counting endpoint, so the plugin must report an +endpoint-level gap that the CPA scheduler treats as availability-neutral. Older +hosts cooled a credential down after a 501 there, turning the following +/v1/messages request into 503 auth_unavailable (CLIProxyAPI issue #7271). +Only loopback mock upstreams and fixture keys are used. +""" +import json +import urllib.error + +import cpa_e2e +from cpa_harness import API, CPAHost + +ROUNDS = 3 + + +def count_tokens(host, body): + try: + host.call("/v1/messages/count_tokens", body, management=False) + except urllib.error.HTTPError as error: + return error.code, error.read().decode("utf-8", "replace") + raise AssertionError("count_tokens must stay unsupported instead of fabricating a token count") + + +def main(): + with CPAHost(cpa_e2e.MockUpstream) as host: + host.credential() + models = host.call(API + "/models/refresh", {})["models"] + host.models(models) + report = [] + for model in ("mock-claude", "mock-chat"): + body = {"model": model, "max_tokens": 64, "messages": [{"role": "user", "content": "hello"}]} + for attempt in range(ROUNDS): + before = len(cpa_e2e.requests) + status, payload = count_tokens(host, body) + assert len(cpa_e2e.requests) == before, "count_tokens must not reach the upstream" + assert status in (404, 501), (status, payload) + try: + result = host.call("/v1/messages", body, management=False) + except urllib.error.HTTPError as error: + raise AssertionError( + f"{model} round {attempt + 1}: /v1/messages failed with HTTP {error.code} " + f"after count_tokens HTTP {status}: {error.read().decode('utf-8', 'replace')[:500]}") + assert "OK from mock" in json.dumps(result), result + report.append({"model": model, "round": attempt + 1, "count_tokens": status, "messages": 200}) + stream = host.call("/v1/messages", {"model": "mock-claude", "max_tokens": 64, "stream": True, + "messages": [{"role": "user", "content": "hello"}]}, management=False, stream=True) + assert "OK from mock" in stream and "message_stop" in stream, stream + credentials = [c for c in host.call("/v8/management/credentials")["files"] if c.get("provider") == "commandcode"] + assert credentials and not any(c.get("unavailable") for c in credentials), credentials + print(json.dumps({"result": "PASS", "rounds": report}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/cpa_thinking_suffix.py b/tests/cpa_thinking_suffix.py new file mode 100644 index 0000000..686a9e6 --- /dev/null +++ b/tests/cpa_thinking_suffix.py @@ -0,0 +1,76 @@ +"""CPA model-name thinking suffixes must work and must not cool down a model. + +CPA_BINARY=/path/to/cli-proxy-api BRIDGE_LIBRARY=/path/to/commandcodebridge.so python3 tests/cpa_thinking_suffix.py + +Clients may select a thinking level as "model(high)" or a budget as +"model(16384)". CLIProxyAPI v8.0.59 strips the suffix before it calls a plugin; +older hosts pass it through. Both must reach the base model with the same +reasoning settings, and native Messages requests must keep the suffix even +though the bridge rebuilds them from the original client body. Only loopback +mock upstreams and fixture keys are used. +""" +import json +import urllib.error + +import cpa_e2e +from cpa_harness import API, CPAHost + +MESSAGES = [{"role": "user", "content": "hello"}] + + +def upstream(host, path, body, stream=False): + before = len(cpa_e2e.requests) + try: + result = host.call(path, body, management=False, stream=stream) + except urllib.error.HTTPError as error: + raise AssertionError(f"{path} {body['model']}: HTTP {error.code} {error.read().decode('utf-8', 'replace')[:500]}") + assert "OK from mock" in (result if stream else json.dumps(result)), result + assert len(cpa_e2e.requests) == before + 1, "expected exactly one upstream request" + return cpa_e2e.requests[-1] + + +def chat(host, model, **extra): + path, body = upstream(host, "/v1/chat/completions", {"model": model, "messages": MESSAGES, **extra}) + assert path.endswith("/chat/completions") and body["model"] == "mock-chat", (path, body) + return body.get("reasoning_effort") + + +def claude(host, model, path="/v1/messages", stream=False, **extra): + request = {"model": model, "max_tokens": 64, "messages": MESSAGES, "stream": stream, **extra} + sent_path, body = upstream(host, path, request, stream=stream) + assert sent_path.endswith("/messages") and body["model"] == "mock-claude", (sent_path, body) + return body.get("thinking"), (body.get("output_config") or {}).get("effort") + + +def main(): + with CPAHost(cpa_e2e.MockUpstream) as host: + host.credential() + host.models(host.call(API + "/models/refresh", {})["models"]) + assert chat(host, "mock-chat") is None + expected = {"high": "high", "HIGH": "high", "none": "none", "0": "none", "512": "minimal", + "1024": "low", "8192": "medium", "24576": "high", "24577": "xhigh", "auto": None, "-1": None, "bogus": None} + for suffix, effort in expected.items(): + assert chat(host, f"mock-chat({suffix})") == effort, suffix + assert chat(host, "mock-chat(low)", reasoning_effort="high") == "low", "suffix overrides the body" + path, body = upstream(host, "/v1/responses", {"model": "mock-chat(low)", "input": "hello"}) + assert body["model"] == "mock-chat" and body.get("reasoning_effort") == "low", (path, body) + for chat_path in (True, False): + route = "/v1/chat/completions" if chat_path else "/v1/messages" + extra = {"max_tokens": 64} if chat_path else {} + assert claude(host, "mock-claude(high)", route, **extra) == ({"type": "adaptive"}, "high"), route + assert claude(host, "mock-claude(none)", route, **extra) == ({"type": "disabled"}, None), route + assert claude(host, "mock-claude(auto)", route, **extra) == (None, None), route + assert claude(host, "mock-claude(16000)", max_tokens=32000) == ({"type": "adaptive"}, "high") + assert claude(host, "mock-claude(low)", thinking={"type": "adaptive"}, output_config={"effort": "max"}) == ({"type": "adaptive"}, "low") + assert claude(host, "mock-claude(medium)", stream=True) == ({"type": "adaptive"}, "medium") + # Unsuffixed requests after every suffixed one prove that no model was cooled down. + assert chat(host, "mock-chat", reasoning_effort="low") == "low" + assert claude(host, "mock-claude", thinking={"type": "adaptive"}) == ({"type": "adaptive"}, None) + upstream(host, "/v1/responses", {"model": "mock-chat", "input": "hello"}) + credentials = [c for c in host.call("/v8/management/credentials")["files"] if c.get("provider") == "commandcode"] + assert credentials and not any(c.get("unavailable") for c in credentials), credentials + print("PASS: thinking suffixes on Chat, Responses and native Messages; no model cooldown") + + +if __name__ == "__main__": + main() \ No newline at end of file From ab3d07e1043cd7e29a1466d664dc625dc1cb8f29 Mon Sep 17 00:00:00 2001 From: Gaoshou101 <188431084+Gaoshou101@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:41:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20=E5=80=99=E9=80=89=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=20OAuth=20=E8=BE=85=E5=8A=A9=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=B9=B6=E6=B1=87=E6=80=BB=E5=85=A8=E9=83=A8=E8=81=94?= =?UTF-8?q?=E6=B5=8B=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #15 首次 CI 中 cpa_native_management.py 在最新 CPA 候选任务里找不到 dist/commandcodebridge-login。补充构建该程序,并改为逐项运行全部联测、 汇总失败后再退出,使报告完整。 --- .github/workflows/build.yml | 20 ++++++++++++-------- docs/VALIDATION.md | 1 + 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bb14aa..891f7cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -290,16 +290,20 @@ jobs: (cd dist/upstream-compatibility && sha256sum --check --ignore-missing checksums.txt && tar -xzf "$archive") python scripts/embed_cpamp_panel.py dist/cpamp-web/*_cpamp-web.zip go build -trimpath -buildvcs=false -buildmode=c-shared -o dist/upstream-compatibility/commandcodebridge.so ./cmd/commandcodebridge + # cpa_native_management.py drives the local OAuth helper from dist/. + CGO_ENABLED=0 go build -trimpath -buildvcs=false -o dist/commandcodebridge-login ./cmd/commandcodebridge-login export CPA_BINARY="$PWD/dist/upstream-compatibility/cli-proxy-api" export BRIDGE_LIBRARY="$PWD/dist/upstream-compatibility/commandcodebridge.so" - python tests/cpa_e2e.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - python tests/cpa_reliability.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - python tests/cpa_diagnostics.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - python tests/cpa_native_management.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - python tests/cpa_round2.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - python tests/cpa_count_tokens.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - python tests/cpa_thinking_suffix.py 2>&1 | tee -a dist/upstream-compatibility/result.txt - python tests/cpa_hot_reload.py 2>&1 | tee -a dist/upstream-compatibility/result.txt + # Run every check so the report is complete, then fail if any of them failed. + failed=() + for test in cpa_e2e cpa_reliability cpa_diagnostics cpa_native_management cpa_round2 cpa_count_tokens cpa_thinking_suffix cpa_hot_reload; do + echo "### $test" >> dist/upstream-compatibility/result.txt + if ! python "tests/$test.py" 2>&1 | tee -a dist/upstream-compatibility/result.txt; then failed+=("$test"); fi + done + if [ "${#failed[@]}" -ne 0 ]; then + echo "FAILED: ${failed[*]}" | tee -a dist/upstream-compatibility/result.txt + exit 1 + fi - name: Record candidate result if: always() shell: bash diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index a35a417..abe0b5f 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -21,6 +21,7 @@ - GitHub 已于 2026-11-02 停用 `macos-14` runner;PR #14 的 macOS 任务即因 `The configuration 'macos-14-arm64-us-default' is not supported` 被取消。darwin-arm64 改用 `macos-15`,并以 `MACOSX_DEPLOYMENT_TARGET=14.0` 保持与 v0.2.11 相同的最低系统(v0.2.11 发布包 dylib 的 `LC_BUILD_VERSION` 为 minos 14.0/SDK 14.5),构建后用 `otool -l` 校验。macOS 构建只能在 CI 验证,本机未运行。 - 新增每周定时运行;最新 CPA 候选任务加跑原生管理、第二轮协议、count_tokens、思考后缀与热重载,定时运行时候选失败会使工作流失败。热升级联测的前一版本改为 v0.2.11。 +- PR #15 首次 CI 中,候选任务(v8.0.59)的 e2e/可靠性/诊断通过,`cpa_native_management.py` 因该任务未构建本地 OAuth 辅助程序 `dist/commandcodebridge-login` 而报 `FileNotFoundError`(测试环境缺失,非插件行为)。已在候选任务中构建该程序,并改为逐项运行全部联测、汇总失败后再退出,避免前一项失败掩盖后续结果。 ### 待验证