
1. 从一条 CI Runner 凭据报错说起凌晨两点流水线在model-smoke-test这一步挂掉日志只有一行401 Unauthorized: invalid x-api-key。本地跑得好好的一进 Runner 就废——这是 CI/CD 工程师最熟悉的一类故障不是模型不行是凭据没进对地方。Runner 是无状态的一次性容器每次 job 都是全新文件系统、全新环境变量表本地~/.claude/settings.json里那份能用的配置在流水线里根本不存在。外部热点也在往同一个方向走据 The Information 报道Apple 正在研发搭载两到四颗未来 M8 Ultra 芯片的企业级 AI 服务器预计 2029 年推出如果落地将是 Xserve 于 2011 年 1 月退役后 Apple 近二十年来首次重返服务器市场。这条新闻对 CI 工程师的启示不是「等 2029 年买机器」而是——模型推理会越来越像标准算力资源接入它的方式也会越来越像接入一个普通的 HTTP 依赖。既然后端迟早会换流水线里就不该把供应商写死在某个私有配置里。所以这篇不讲新闻只讲一件可跟做的事把 CI Runner 的模型调用凭据抽出来统一走 TaoToken。第一步是去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentci-runner-intro 拿到属于你团队的 Key然后把 Base URL 固定填成https://taotoken.net/api。下面给出一份可以直接粘进仓库的 Runner 环境变量片段以及 Claude Code、Codex 两条工具链在流水线里的调用示例。2. 为什么 Runner 的凭据必须在「平台侧」创建而不是在本地导出一份先明确一个工程原则本地凭据和 CI 凭据必须是两套。这不是洁癖是三个现实约束。第一可撤销性。本地 Key 泄了你只能自己换掉影响面是整个团队CI Key 泄了你在控制台点一下禁用当天所有流水线立刻停止扣费。两套凭据 两个独立的爆炸半径。第二审计归属。CI 里的调用应该能被单独统计哪个仓库、哪条分支、哪个 job 在消耗额度。如果 CI 复用开发者个人 Key账单里所有人的流量会糊成一坨月底没人能解释某笔异常消耗来自哪里。第三权限最小化。CI Runner 只需要「调用模型」这一件事不需要管理 Key、不需要看账单、不需要改配置。给流水线的凭据越窄出问题的面越小。据 The Information 的这条报道未来企业级 AI 算力会以「服务器集群」的形态供应而在那之前以 API 形态供应的推理能力已经是当前最现实的选择。把供应商抽象成一个 Base URL才是让流水线具备「后端可换」能力的正确做法。创建 CI 专用 Key 的入口在控制台https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentci-runner-console 。路径是登录后进入 API Keys 页面新建一个 Key命名建议带上仓库或用途例如ci-gha-model-test、ci-gitlab-smoke。命名规范不是形式主义——出问题时你需要在列表里一眼定位是哪个 job 在冒烟。3. Runner 环境变量片段一份可以跨平台复用的模板CI 平台五花八门但底层都是「把密钥注入成环境变量」。所以先写一份与平台无关的变量清单再讲各平台怎么喂进去。推荐的最小变量集只有四个# .env.ci —— 仅作为变量清单参考真实值放平台 Secret不要提交此文件 # 模型服务端地址所有工具共用同一个 Base URL TAOTOKEN_BASE_URLhttps://taotoken.net/api # 流水线专用 Key占位符真实值由 CI Secret 注入 TAOTOKEN_API_KEYYOUR_API_KEY # Claude Code 读取的变量名 ANTHROPIC_BASE_URLhttps://taotoken.net/api ANTHROPIC_AUTH_TOKENYOUR_API_KEY # 默认模型按团队约定填 ANTHROPIC_MODELYOUR_MODEL_NAME四个注意点YOUR_API_KEY是占位符绝对不要把它 commit 进仓库。哪怕仓库是私有的Git 历史也是长期可读的。TAOTOKEN_BASE_URL和ANTHROPIC_BASE_URL指向同一个地址前者是给自定义脚本用的中性命名后者是 Claude Code 认的固定名字。两者分开是为了让脚本不依赖工具专有变量名。不要给不同工具塞同一组变量名。Claude Code 认ANTHROPIC_*Codex 认自己的 provider 配置——把ANTHROPIC_AUTH_TOKEN写进 Codex 的配置里它不会报「变量名错了」它会直接忽略然后你去查半天为什么没生效。模型名单独抽成变量。当你要做 A/B 对比、或者做降级策略时只改一个变量就能切换不用动代码。3.1 GitHub Actions 注入方式# .github/workflows/model-smoke.yml name: model-smoke on: pull_request: branches: [main] jobs: smoke: runs-on: ubuntu-latest env: TAOTOKEN_BASE_URL: https://taotoken.net/api ANTHROPIC_BASE_URL: https://taotoken.net/api ANTHROPIC_AUTH_TOKEN: ${{ secrets.TAOTOKEN_CI_KEY }} ANTHROPIC_MODEL: ${{ vars.ANTHROPIC_MODEL }} steps: - uses: actions/checkoutv4 - name: 校验凭据是否存在不打印值 run: | if [ -z $ANTHROPIC_AUTH_TOKEN ]; then echo ::error::TAOTOKEN_CI_KEY 未配置请在仓库 Secrets 中添加 exit 1 fi echo 凭据已注入长度 ${#ANTHROPIC_AUTH_TOKEN} - name: 最小连通性检查 run: | curl -sS -o /dev/null -w %{http_code}\n \ -H Authorization: Bearer $ANTHROPIC_AUTH_TOKEN \ -H Content-Type: application/json \ $ANTHROPIC_BASE_URL/models注意secrets和vars的区别Key 走secrets自动打码模型名这类非敏感配置走vars。校验步骤里只打印长度、不打印内容是因为 CI 日志会被归档、会被同事翻阅、会在失败时被贴到群里。3.2 GitLab CI 注入方式# .gitlab-ci.yml stages: [test] model-smoke: stage: test image: alpine:3.20 variables: TAOTOKEN_BASE_URL: https://taotoken.net/api ANTHROPIC_BASE_URL: https://taotoken.net/api ANTHROPIC_MODEL: YOUR_MODEL_NAME script: - apk add --no-cache curl - test -n $TAOTOKEN_CI_KEY || (echo TAOTOKEN_CI_KEY 未设置 exit 1) - export ANTHROPIC_AUTH_TOKEN$TAOTOKEN_CI_KEY - | curl -sS -o /dev/null -w http%{http_code}\n \ -H Authorization: Bearer $ANTHROPIC_AUTH_TOKEN \ $ANTHROPIC_BASE_URL/models rules: - if: $CI_PIPELINE_SOURCE merge_request_eventTAOTOKEN_CI_KEY建议在 GitLab 项目 Settings → CI/CD → Variables 里设置勾选Masked如果流水线跑在受保护分支再加Protected这样非保护分支的 job 拿不到它。3.3 Jenkins 注入方式pipeline { agent any environment { ANTHROPIC_BASE_URL https://taotoken.net/api ANTHROPIC_MODEL YOUR_MODEL_NAME ANTHROPIC_AUTH_TOKEN credentials(taotoken-ci-key) } stages { stage(model-smoke) { steps { sh set -eu [ -n $ANTHROPIC_AUTH_TOKEN ] || { echo 凭据为空; exit 1; } curl -sS -o /dev/null -w http%{http_code}\\n \ -H Authorization: Bearer $ANTHROPIC_AUTH_TOKEN \ $ANTHROPIC_BASE_URL/models } } } }Jenkins 的credentials()会自动把值替换成****日志里不会泄露。这是 Jenkins 相比手工withEnv更值得用的唯一理由。到这一步你已经有了一份可跨平台复用的凭据骨架。接下来是工具侧怎么消费它。4. Claude Code 在流水线里的 settings.json 与 ANTHROPIC_* 变量Claude Code 的配置优先级大致是命令行参数 环境变量 项目级settings.json 用户级settings.json。CI 场景下最稳的做法是——项目级settings.json只写非敏感项敏感项全部走环境变量。项目根目录建一个.claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_MODEL: YOUR_MODEL_NAME }, permissions: { allow: [ Read, Grep, Glob ], deny: [ Bash(rm:*), Bash(curl:* | sh) ] } }这里刻意没有把ANTHROPIC_AUTH_TOKEN写进文件。它由 Runner 的环境变量提供。这样做的收益很直接同一个settings.json可以提交进仓库本地开发、CI、预发三套环境共用同一份工具配置只有密钥来源不同。如果你确实需要在settings.json里声明这个键名比如为了明确告诉协作者需要哪个变量可以写成引用形式并配套一份说明但值永远留空由环境注入{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: , ANTHROPIC_MODEL: YOUR_MODEL_NAME } }在流水线里执行一次非交互调用最小形态是这样#!/usr/bin/env bash set -euo pipefail : ${ANTHROPIC_BASE_URL:?未设置 ANTHROPIC_BASE_URL} : ${ANTHROPIC_AUTH_TOKEN:?未设置 ANTHROPIC_AUTH_TOKEN} # Claude Code 非交互模式一次提问、一次输出、退出 claude -p 请检查以下 diff 是否引入了明显的空指针风险只回答风险点不要重写代码。 \ /tmp/pr.diff \ /tmp/review.txt 2/tmp/review.err echo ---- 审查结果 ---- cat /tmp/review.txt-pprint模式是 CI 里唯一该用的模式。交互模式在无 TTY 的 Runner 上会直接挂住直到 job 超时。几个容易踩的坑ANTHROPIC_API_KEY与ANTHROPIC_AUTH_TOKEN的区别前者是 Anthropic 官方语义下的密钥变量后者用于第三方兼容端点场景。走 TaoToken 时优先用ANTHROPIC_AUTH_TOKEN如果某个版本的工具只认ANTHROPIC_API_KEY再补一个同值变量即可但不要两个都设成不同值否则行为取决于工具内部的读取顺序非常难 debug。Base URL 末尾不要带斜杠。https://taotoken.net/api和https://taotoken.net/api/在某些 HTTP 客户端里会拼出//v1/...服务端返回 404而日志里只看到「路径不存在」你会以为是 Key 的问题。超时。CI 网络比本地差建议给单次调用设 60–120 秒上限并在脚本里显式失败而不是让 job 卡到平台默认的 6 小时。5. Codex 侧config.toml 与「不要把 ANTHROPIC_* 套过来」这是本文最想强调的一点Codex 不读ANTHROPIC_*。把 Claude Code 的那套变量直接复制到 Codex 的配置里不会报错只会静默走默认供应商或者直接失败然后你在 CI 里看到一句含糊的「provider not configured」。Codex 的配置写在~/.codex/config.tomlCI 里通常通过 Runner 的环境变量CODEX_HOME指向仓库内的一个目录把配置随代码一起版本化# .codex/config.toml model YOUR_MODEL_NAME model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat对应地在流水线里注入export CODEX_HOME$PWD/.codex export TAOTOKEN_API_KEY$TAOTOKEN_CI_KEY要点说明env_key写的是环境变量的名字不是值。Codex 会在运行时去环境里取这个名字对应的值。把值写进config.toml是常见误操作等于把密钥提交进了仓库。wire_api按你实际使用的接口协议填。填错的表现是「请求发出去了、返回结构解析不了」看起来像服务端问题其实是客户端协议不匹配。如果你的客户端版本要求 OpenAI 兼容路径可以尝试在base_url后追加/v1但这属于客户端侧的路径拼接差异统一入口仍然是https://taotoken.net/api。先用不带后缀的地址验证连通性再按客户端要求调整。一次非交互调用的示例#!/usr/bin/env bash set -euo pipefail : ${TAOTOKEN_API_KEY:?未设置 TAOTOKEN_API_KEY} codex exec \ --config $PWD/.codex/config.toml \ 根据 /tmp/pr.diff 生成三条边界测试用例输出为纯文本列表。 \ /tmp/codex-cases.txt cat /tmp/codex-cases.txtClaude Code 和 Codex 在同一仓库共存时建议目录结构这样放避免互相污染repo/ ├── .claude/ │ └── settings.json # 只放非敏感项 ├── .codex/ │ └── config.toml # env_key 引用环境变量 ├── scripts/ │ ├── ci-claude-smoke.sh │ └── ci-codex-smoke.sh └── .github/workflows/ └── model-smoke.yml6. CC Switch 三件套让本地和 CI 用同一份供应商定义本地开发时你可能要在多个供应商之间来回切CI 里则要求绝对确定只有一套生效。CC Switch 这类配置切换工具的价值就是把「供应商定义」抽成三件套Base URL—— 统一填https://taotoken.net/apiAPI Key—— 本地用个人 KeyCI 用流水线专用 KeyYOUR_API_KEY的占位位置由 Secret 替换模型名—— 与ANTHROPIC_MODEL/config.toml里的model保持一致三件套的关键不是「能切」而是切完之后 Claude Code 和 Codex 读到的定义必须等价。工程上落地成两条规则任何一处base_url改了必须同步改另一处。建议在仓库里放一个provider.env作为单一事实来源两个工具都从它读而不是各自维护。模型名不允许硬编码在脚本里。脚本里出现字面量模型名的 PR 一律拦下改成读$ANTHROPIC_MODEL。一个把三件套集中管理的做法# scripts/provider.env —— 被 CI 和本地脚本共同 source export TAOTOKEN_BASE_URLhttps://taotoken.net/api export ANTHROPIC_BASE_URL$TAOTOKEN_BASE_URL export ANTHROPIC_MODEL${ANTHROPIC_MODEL:-YOUR_MODEL_NAME} # 注意这里不设置任何 KEYKEY 永远来自外部注入# scripts/ci-claude-smoke.sh #!/usr/bin/env bash set -euo pipefail source $(dirname $0)/provider.env : ${ANTHROPIC_AUTH_TOKEN:?CI 环境未注入密钥} claude -p 输出 OK 两个字符不要输出其他内容。 | head -c 200这样做的收益在排障时最明显当流水线失败你只需要确认「环境变量有没有进来」「地址对不对」两件事而不需要逐个工具去翻它自己的配置文件。7. 把模型测试挂进流水线的三个可运行模式前面给的是连通性验证。真正有工程价值的是把模型调用嵌进既有流程。下面三个模式按侵入性从低到高排列。7.1 模式一PR 提交即冒烟最低侵入只在涉及提示词、配置、模型名的改动上触发on: pull_request: paths: - prompts/** - .claude/** - .codex/** - scripts/ci-*.sh这样避免每次改一行文档就跑一次模型调用额度用在刀刃上。7.2 模式二diff 摘要作为 Artifact 落盘不阻塞合并只产出信息供人看- name: 生成 diff 摘要 run: bash scripts/ci-claude-smoke.sh - uses: actions/upload-artifactv4 if: always() with: name: model-review path: /tmp/review.txt retention-days: 7if: always()很重要模型调用失败时你更需要看它失败前的输出。7.3 模式三固定用例集做回归准备一份小而稳的用例文件每次流水线都跑只要有一条不通过就红[ { id: case-001, prompt: 把这个 JSON 的字段名转成小写下划线风格, expect_contains: user_id }, { id: case-002, prompt: 给定输入 3输出它的平方只输出数字, expect_contains: 9 } ]#!/usr/bin/env bash set -euo pipefail source $(dirname $0)/provider.env : ${ANTHROPIC_AUTH_TOKEN:?缺少密钥} fail0 while IFS read -r prompt; do out$(claude -p $prompt 2/dev/null || true) echo prompt$prompt echo output$out [ -n $out ] || fail1 done (jq -r .[].prompt testcases/model-regression.json) exit $fail回归用例的原则判据要确定、输出要短、不依赖外部数据。模型输出本质上有波动用例设计得越宽松流水线越不容易变成「狼来了」。8. CI 里最常见的四类失败以及怎么定位第一类401 / 403。九成是密钥没注入或注入了空值。先跑这段自查它不会泄露密钥echo base${ANTHROPIC_BASE_URL:-empty} echo key_len${#ANTHROPIC_AUTH_TOKEN} echo model${ANTHROPIC_MODEL:-empty}key_len0就是注入失败key_len正常但仍 401才去控制台确认这个 Key 是否被禁用、是否已过期。第二类404。通常是 Base URL 写错或有尾部斜杠。确认用的是https://taotoken.net/api不带结尾/也不要手工拼成/api/v1/...以外的路径。第三类超时。CI 出口网络绕路或者单次请求内容过大。先降规模——把完整 diff 截断到前 200 行再试能通就是负载问题不能通才是链路问题。第四类本地能跑、CI 不能跑且报错含糊。这类几乎都是配置优先级问题。Claude Code 会按「命令行 环境变量 项目 settings 用户 settings」的顺序合并Runner 上如果镜像里预置了一份用户级配置它可能覆盖你以为生效的项。定位方法是在 CI 里显式导出所有相关变量并在脚本开头打印一遍地址和模型名。排查时把日志分成三层会快很多L1 凭据层变量是否存在、长度是否正常、是否被平台打码 L2 网络层地址是否可达、状态码是多少、耗时多少 L3 语义层返回内容是否符合预期、用例判据是否过严大部分「模型有问题」的工单最后都落在 L1。9. 一条可落地的接入顺序如果你准备今天就把这件事推进到可合并状态按这个顺序做每一步都能独立验证到控制台创建流水线专用 Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentci-runner-create-key 命名带ci-前缀只授调用权限。把 Key 存进 CI 平台的 SecretGitHub Secrets / GitLab Masked Variable / Jenkins Credentials一个平台一份不要跨平台复用同一个值。在仓库里加scripts/provider.env把 Base URL 固定为https://taotoken.net/api模型名做成可覆盖变量。加一个最小连通性 job只做 HTTP 状态码校验先让它绿。再挂 Claude Code 或 Codex 的实际调用先从 diff 摘要这类不阻塞的用途开始。稳定运行一周后再考虑把它升级成阻塞式门禁。按这个节奏走你不会在第一天就把主分支堵死也不会在出问题时无从下手。回到开头那条凌晨两点的报错它不是模型不可用只是凭据没有以 Runner 能理解的方式到达。这也是为什么把供应商抽象成「一个 Base URL 一个可轮换的 Key」值得认真做——无论未来企业级推理跑在谁的机房里你的流水线只需要改一个变量的值。需要继续往下走的话想先在网页上验证模型是否可用https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentci-runner-chat想给团队配固定的编码额度方案https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentci-runner-plan想直接创建 CI 专用 Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentci-runner-keysClaude Code 的完整配置与参数说明https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentci-runner-doc所有配置里Base URL 记住一个值就够了https://taotoken.net/apiKey 的位置永远留给YOUR_API_KEY由 CI Secret 在运行时替换。