LiteLLM 的 Claude Code 兼容性矩阵每日自动刷新:基于 GCP VM + systemd 的 cron 流水线全解
本文围绕 LiteLLM 仓库内 tests/e2e/claude_code/cron_vm/ 一套完整的"兼容性矩阵定时填充器(compatibility-matrix populator)"展开:它不跑在 GitHub Actions 上,而是每天在专有 GCP VM litellm-compatibility-matrix-populator 上,把最新稳定版 LiteLLM 网关与 Claude Code CLI 组合起来,跑一遍横跨 Anthropic、Bedrock、Vertex AI、Azure/Foundry、OpenAI 等云后端的 e2e 兼容性矩阵,再自动把结果以 PR 形式发布到 litellm-docs 文档仓库。读者读完将掌握这套生产级 cron 的完整设计(目录职责、9 步执行流程、回归门控自动合并、systemd 单元与安全加固、一次性搭建与日常运维排障),并可迁移复用到自己的"每日自动化测试 + 自动发 PR"场景。
一、为什么用一台常驻 VM 而不是 GitHub Actions
兼容性矩阵的产出物是一张"Claude Code 各功能 × 各云后端"的通过/失败表(compatibility-matrix.json),每天刷新一次。项目最初也想用 GitHub Actions cron,但最终选择了一台常驻 GCP VM(名为 litellm-compatibility-matrix-populator)来承载每日任务,权衡如下:
- 真实 VM 便于
gh auth login:任务需要把测试结果以 PR 形式提交到BerriAI/litellm-docs文档仓库。在 VM 上可以直接用一个已是该文档仓库 collaborator 的账号登录 GitHub CLI,而不必去申请一个带pull-requests: write权限的 GitHub App 凭据。 - 持久状态复用:VM 上长期保留一个工作树
~/litellm-cron-worktree/及其.venv。每天运行时只需做一次快速的git checkout换 tag + 增量式uv sync,而不是每次从零git clone+ 冷启动装依赖。 - 零 Docker 依赖:代理不需要容器化,直接用
uv run litellm以子进程方式拉起即可。 - 代价一:VM 必须开机。虽然 systemd 定时器的
Persistent=true能在短暂停机后补跑,但连续多天宕机会让矩阵过期,直到 VM 恢复。 - 代价二:云厂商凭据放在 VM 文件系统(
/etc/litellm-compat-matrix.env)而非 GitHub Secrets。因此应把该 VM 视为与 CI runner 爆炸半径相当的可信环境来治理。
需要留意目录的迁移历史:cron_vm/ 原本位于 tests/claude_code/cron_vm/(与独立的 tests/claude_code/ 套件配对),如今改为运行由仓库维护的 tests/e2e/claude_code/ 套件,pytest 环境接口也随之变化:runner 导出的环境变量从 LITELLM_PROXY_BASE_URL / LITELLM_PROXY_API_KEY 改为 LITELLM_PROXY_URL / LITELLM_MASTER_KEY;azure 列读取 AZURE_AI_API_KEY / AZURE_AI_API_BASE(旧名为 AZURE_FOUNDRY_*);GPT 列需要 OPENAI_API_KEY 与 AZURE_API_BASE / AZURE_API_KEY——完整清单见 litellm-compat-matrix.env.example。
二、目录布局:6 个文件各司其职
cron_vm/ 目录极简,职责划分非常清晰,README 用一张表说明:
| 文件 | 用途 |
|---|---|
| run_daily.sh | 真正的 cron 任务本体:解析版本、更新工作树、启动代理、运行 pytest、生成 JSON、打开/更新文档 PR、清扫过期的 compat-matrix PR。 |
| build_matrix.py | 一个精简的 Python CLI,封装 claude_code.matrix_builder.build_from_paths。仅因 bash 脚本需要某种方式做逐格聚合,而该构建器本来就是 Python 实现。 |
| check_regressions.py | 精简的 Python CLI,封装 claude_code.matrix_builder.find_regressions。把刚构建的矩阵与当前已发布矩阵做 diff,若出现任何单元格绿→红翻转则以退出码 3 结束,从而门控自动合并。 |
| litellm-compat-matrix.service | systemd oneshot 服务单元,负责调用 run_daily.sh。 |
| litellm-compat-matrix.timer | OnCalendar=*-*-* 06:00:00 UTC、Persistent=true 的定时器。 |
| litellm-compat-matrix.env.example | /etc/litellm-compat-matrix.env 的模板。 |
两个 Python CLI 的哲学一致:真正的逻辑都在套件侧的 matrix_builder.py 中(见 matrix_builder.py),CLI 只做参数解析与 I/O,并把结果映射成 bash 可以分支判断的退出码。例如 check_regressions.py 约定:0 表示无绿→红回归(可安全自动合并)、3 表示发现一个或多个绿→红回归(需人工评审)、2 为 argparse 用法错误;且 --old 文件允许不存在——首次发布没有基线可对比时直接退出 0,视为"无回归"。
由于套件内部模块是以 tests/e2e/ 作为 sys.path 根来解析的(tests/e2e/ 没有 __init__.py,而 claude_code/ 有),这两个 CLI 都在导入前通过 sys.path.insert(0, parents[2]) 自举同一个根路径(见 build_matrix.py)。
三、run_daily.sh 的 9 步核心流水线
run_daily.sh 是整条流水线的引擎(脚本以 set -Eeuo pipefail 运行,任何命令缺失都会提前报错)。其默认环境变量均可用同名环境变量覆盖,生产接线见 systemd 单元。以下是脚本注释与实现中完整保留的 9 个环节:
1. 解析最新 LiteLLM 正式版 tag
通过 GitHub Releases API(curl | jq,翻页获取)解析最新的最终版 tag(形如 vX.Y.Z,跳过 -rc.N / -dev.N 预发布)。这里的实现细节值得一提(见 run_daily.sh):
- LiteLLM 已从旧的
vX.Y.Z-stable命名迁移到 PEP 440 风格:正式版是裸vX.Y.Ztag,预发布带-rc.N/-dev.N段(旧-stable/-stable.patch.N已冻结在 v1.83.x)。 - 用正则
^v[0-9]+\.[0-9]+\.[0-9]+$过滤,并用数值化的sort_by(capture(...))保证1.10 > 1.9。 - Releases API 默认每页 30 条,而 LiteLLM 每天可能发多个预发布,因此脚本逐页拉取并封顶 5 页(500 条 release),只要某一页已出现正式版 tag 就提前停。
2. 读取本机 Claude Code CLI 版本
通过 claude --version 读取。cron 不会自动升级 CLI——运营人员想测试新版本 CLI 时,需在带外手动执行 npm install -g @anthropic-ai/claude-code@latest。
3. 把持久工作树更新到目标 tag,并"重塑"测试套件
工作树位于 ~/litellm-cron-worktree/,更新序列为 git fetch --tags --force → git reset --hard → git clean -fdx -e .venv -e .uv-bin -e .uv-python → git checkout --force <tag>。保留 .venv 是为了让随后的 uv sync --frozen 变成增量同步。随后脚本会重塑测试套件:工作树里的 tests/e2e/ 是从开发检出(dev checkout)重新拷入的——只复制 claude_code/ 套件以及它 import 的五个共享传输辅助文件(proxy_client.py、e2e_http.py、models.py、e2e_config.py、transport.py),这样 cron 永远是用今天的测试去测最新的稳定代理。
为什么不能直接用该 tag 自带的 tests/e2e/?因为 tag 自带的是完整 EKS e2e 测试框架,其顶层 conftest.py import 了 e2e_db、lifecycle、otel_client 等稳定版 venv 未安装的模块;整树拷入会让 pytest 收集阶段直接爆炸。因此重塑是一个"先 rm -rf tests/e2e/、再精确拷入 claude_code 套件 + 五个共享 helper"的过程(见 run_daily.sh)。
同一环节还处理了 uv 版本钉死:LiteLLM 的 pyproject.toml 在 [tool.uv] required-version 字段钉死了确切 uv 版本,系统 uv 若版本不符会拒绝 sync。脚本用 awk 解析出该字段,把对应版本 uv 下载并缓存到工作树内 .uv-bin/,且先校验 Astral 官方 .sha256 摘要再解压执行(见 run_daily.sh),封堵了"把远端二进制直接管道进 tar"的供应链信任缺口。最后执行:
uv sync --frozen --group proxy-dev --extra proxy --python 3.12
其中 --extra proxy 拉入 fastapi/uvicorn 让 uv run litellm 真正能提供 HTTP 服务,--group proxy-dev 带来 pytest 等套件依赖,--python 把 venv 钉到 Python 3.12。
4. 后台拉起代理并等待就绪
代理以 setsid 后台进程运行在 4100 端口(故意避开开发者常用的 :4000,防止 SSH 进同一台 VM 的开发者代理与 cron 冲突),随后轮询 http://127.0.0.1:4100/health/liveliness 直至就绪(45 次 × 2 秒上限)。
两个安全/工程细节值得注意(见 run_daily.sh):
- 代理只绑定 loopback(
--host 127.0.0.1)。因为只有本机 pytest 会访问它,而默认LITELLM_MASTER_KEY=sk-cron-matrix是固定可预测值,若不绑 loopback,任何能触达该 VM 端口的外部方都能通过默认 key 认证并烧掉上游云厂商凭据。 - 用
setsid让代理处于独立会话/进程组,并把 pgid 写入${WORKDIR}/proxy.pid;退出时cleanup()能以负 pid 形式对整个进程组发 SIGTERM,必要时再用pgrep兜底 SIGKILL 掉任何仍在监听该端口的残留进程、用ss -K清端口、最后删除临时目录。trap cleanup EXIT INT TERM保证正常退出、收到信号、甚至部分失败(代理已起但 pid 文件陈旧)时都能收敛。
5. 运行 pytest,失败变成 fail 单元格而非脚本错误
代理就绪后,在 tests/e2e/claude_code/ 上运行 pytest,注入以下环境:
LITELLM_PROXY_URL=http://127.0.0.1:${PROXY_PORT}LITELLM_MASTER_KEY=sk-cron-matrixCOMPAT_RESULTS_PATH=${WORKDIR}/compat-results.json(conftest 钩子据此写出每个测试的结果工件)
测试的退出码有特殊语义:0 = 全绿,1 = 存在测试失败(映射成 fail 单元格,不算脚本错误);而 >=2 表示中断/内部错误/无测试收集,属于"部分运行",其缺失单元格会被发布成 not_tested,因此脚本对 PYTEST_EXIT >= 2 直接 die,拒绝发布不完整的矩阵。调试时可用 PYTEST_K 环境变量把运行收敛到某个单元格(例如 basic_messaging_non_streaming and anthropic),测试参数中还会 --ignore-glob=*_unit_tests* 跳过套件内的单元测试目录(见 run_daily.sh)。
6. 构建 compatibility-matrix.json
把 pytest 结果工件 + manifest 交给 build_matrix.py:
uv run python build_matrix.py \
--manifest tests/e2e/claude_code/manifest.yaml \
--results compat-results.json \
--output compatibility-matrix.json \
--litellm-version "${LITELLM_VERSION}" \
--claude-code-version "${CLAUDE_CODE_VERSION}"
CLI 在内部自动盖上 generated_at(UTC 时间戳),并调用 matrix_builder.py 的 build_from_paths 完成纯函数式的矩阵构建。该模块被刻意设计为无网络/子进程/文件系统副作用,方便 golden-file 单测直接喂内存输入。
7. 打开或更新 litellm-docs 文档仓库的 PR
gh repo clone将BerriAI/litellm-docs浅克隆进临时目录;- 在确定性分支名上工作:
compat-matrix/<litellm版本>-<claude-code版本>-<UTC日期>; - 用
--force直接把分支推送到BerriAI/litellm-docs本体(不是 fork)——因为mateo-berri机器人的 token 在该仓库有写权限,属于同仓库分支 PR; gh pr create创建 PR。若当天重跑,则快进同一分支,gh pr create遇到 "a pull request for branch ... already exists" 视为成功(不重复开 PR);若矩阵 JSON 与文档分支字节级一致,则直接跳过推送与 PR。
PR 标题格式为 chore(compat-matrix): refresh for <litellm> + claude-code <claude>,正文包含 litellm_version / claude_code_version / generated_at 字段表以及按 feature 生成的逐格状态摘要(jq 从 JSON 现场渲染,便于评审者快速 triage)。此类机器人 PR 不再需要第二位人类评审,是否合入完全由下一步的回归门控决定。
8. 用回归检查门控自动合并
自动合并开启前,check_regressions.py 把新矩阵与当前 main 上已发布的矩阵做 diff(发布前先 cp 快照旧矩阵到临时路径,见 run_daily.sh)。仅当没有任何单元格发生绿→红翻转时才允许 gh pr merge --auto --squash:
- 允许的状态迁移:红→绿、绿→绿、红→红;
- 预先存在的红格(例如某供应商 API 额度耗尽)属于红→红,不阻塞每日 PR 正常流动;
- 只有
pass→fail的翻转才构成回归。
当检测到回归时,PR 依然会被打开/更新(正文顶部追加 > [!WARNING] 横幅并点名违规单元格),但自动合并保持关闭——且若当天较早一次干净运行曾开启过自动合并,会被显式关闭并通过 gh pr view --json autoMergeRequest 回读确认,确保"带回归的矩阵绝不处于可自动合并状态"。
门控采用 fail-closed 策略:如果检查器自身出错(非 0 非 3 的退出码),同样扣留自动合并,防止门控的 bug 静默放行一次回归。由于 BerriAI/litellm-docs 仓库在 repo 层面禁用了 merge-commit 与 rebase、只允许 squash,AUTO_MERGE_METHOD 默认值即 squash。
9. 清扫过期的 compat-matrix PR
今天的分支/PR 就绪后,脚本会列出文档仓库所有仍打开且 head 分支以 compat-matrix/ 开头的 PR,把除今天之外的全部关闭(并删除机器人所属分支),保证任意时刻最多只有一个 compat-matrix PR 处于打开状态——即最新的那个。这是非破坏性的:PR 记录与其回归报告仍可浏览。此步骤只在今天的 PR 已存在后执行(前面任何 die 都会跳过它),因此发布失败永远不会把队列清到零;清扫本身失败(限流等)也仅记 WARN,留给下次重试。
四、回归门控与单元格语义的底层原理
要理解门控为何"只盯绿→红",需要先看懂矩阵单元格是如何产生的:
单元格状态机:每个 (feature, provider) 单元格的状态取值于 {pass, fail, not_applicable, not_tested}(定义于 matrix_builder.py)。没有测试跑到、且未显式声明 not_applicable 的空单元格会被填成 not_tested。
"三个模型档位全部通过"才算绿:矩阵的每个单元格实际由三次 Claude 模型调用支撑(Haiku/Sonnet/Opus 三档)。conftest 提供 compat_result fixture,测试用 .set({"status": "pass"}) 上报单结果;多模型并发测试用 .add(...) 逐档上报,让每档模型各占结果工件中的一行。聚合时若多档结果并存,单元格只有全档通过才为 pass,否则为 fail 并在 error 中浮出首个出问题的档位(见 matrix_builder.py 与 conftest.py 的 docstring)。因此"任一档位在某个新版本代理下坏掉"都会立刻把绿格染红,这正是门控要拦截的信号。
行列如何推断:(feature, provider) 由测试文件路径推断——父目录名即 feature_id(与 manifest.yaml 对齐),test_ 前缀之后的文件主干即 provider id(见 conftest.py)。这样避免了容易漂移的逐文件元数据。
矩阵的当前形状:manifest(见 manifest.yaml)声明了 9 个 provider 列(anthropic、bedrock_invoke、bedrock_converse、vertex_ai、azure、openai、azure_openai、bedrock_mantle、vertex_ai_gpt)与 16 个 feature 行(basic_messaging_non_streaming / _streaming、tool_use、prompt_caching_5m、vision、thinking、tool_use_streaming、thinking_with_tool_use、pdf_input、prompt_caching_1h、web_search、structured_outputs、count_tokens、tool_search、passthrough、long_context_1m)。其中 vertex_ai_gpt 是静态 not_applicable 列:GCP 不提供闭源 GPT-5.6 系列(Model Garden 只有开源的 gpt-oss),该列把这一空白显式记录下来而非标红。发布物结构可参考仓库内的 sample_compatibility-matrix.json(含 schema_version、generated_at、litellm_version、claude_code_version、providers、features 等字段)。
代理的别名层:测试只认识别名(如 claude-haiku-4-5-bedrock-invoke),真正把别名映射到各家云上游 model id / region / 凭据的是 LiteLLM 代理层的 test_config.yaml。因此"新增 (feature, provider) 单元格"是测试仓库内的三步改动(manifest + 测试文件 + 别名),而"更换某个单元格所打的上游模型"在这里改一处即可、无需动测试。代理配置还开启了 forward_client_headers_to_llm_api: true,把 Claude Code 发送的供应商专属 header(如 anthropic-beta)原样转发到上游,确保被测的线缆形状与真实客户一致。
五、systemd 单元:定时器 + oneshot 服务
定时器 litellm-compat-matrix.timer
OnCalendar=*-*-* 06:00:00 UTC
Persistent=true
RandomizedDelaySec=10min
Unit=litellm-compat-matrix.service
- 每天 06:00 UTC 触发,选这个时刻是为了让美欧时区的运营人员在一天工作开始时看到新鲜 PR(与原 GitHub Actions cron 时间一致)。
Persistent=true是每日任务想要的性质:VM 关机/挂起错过的运行,会在下次定时器启动时补跑,而不是再等 24 小时。RandomizedDelaySec=10min把触发抖动 10 分钟,若将来有多条类似流水线共置同一 VM,可分散负载。
服务单元 litellm-compat-matrix.service
服务是 Type=oneshot,因为定时器语义要表达"每天跑一次、跑完即退",没有需要常驻守护的进程(见 litellm-compat-matrix.service):
[Service]
Type=oneshot
User=mateo
Group=mateo
EnvironmentFile=-/etc/litellm-compat-matrix.env
LoadCredential=github-token:/etc/litellm-compat-matrix-github-token
Environment=PATH=/home/mateo/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=HOME=/home/mateo
WorkingDirectory=/home/mateo/litellm/litellm
ExecStart=/home/mateo/litellm/litellm/tests/e2e/claude_code/cron_vm/run_daily.sh
TimeoutStartSec=90min
Restart=no
关键设计:
- 用户与路径:以
mateo运行,路径硬编码为/home/mateo而非 systemd 的%h说明符。原因写在了单元注释里:系统单元中%h是按 PID 1(manager)的家目录即/root展开的,而不是按User=指令展开,这会导致ReadWritePaths指向不存在的/root/.cache,命名空间设置直接以status=226/NAMESPACE失败。同时显式补 PATH(uv、claude装在mateo的~/.local/bin下)与 HOME。 - 凭据分离:provider 凭据与 gh/PROXY_PORT 覆盖放在
EnvironmentFile=-/etc/litellm-compat-matrix.env(-前缀表示文件缺失时服务仍可启动,但没有 provider 凭据会在首个请求处失败);而发布用 PAT 通过LoadCredential=github-token:/etc/litellm-compat-matrix-github-token映射,故意不进 EnvironmentFile。原因见下一节的安全分析。 - 超时与重试:
TimeoutStartSec=90min为冷启动留足余量(冷运行要 clone + 对锁文件做一次全新 uv sync,2 vCPU 的 VM 上可能耗时数分钟,之后还要跑全 feature × provider 网格打多家云);Restart=no是因为失败不该立即自动重试,下一次定时器触发才是正确的重试节奏,且同日重跑是幂等的。 - 安全加固:
NoNewPrivileges=true、ProtectSystem=strict、ProtectHome=read-only、PrivateTmp=true;ReadWritePaths白名单只放工作树~/litellm-cron-worktree、uv 轮子缓存~/.cache、claude CLI 会话状态~/.claude、gh 主机配置~/.config/gh与/tmp——流水线其余只读消费 litellm 检出与 env 文件。
六、一次性 VM 环境搭建
以下步骤在 cron VM 上以 mateo 用户执行(命令来自 cron_vm 目录 配套文档,外部下载地址以占位符示意,实际安装时替换为对应官方安装入口):
# 1. 工具链:git、node/npm(claude CLI 依赖)、jq、curl、uv、gh
sudo apt-get update
sudo apt-get install -y git nodejs npm jq curl
curl -LsSf <uv 官方安装脚本> | sh
sudo apt-get install -y gh
# 2. Claude Code CLI(cron 不会自动升级它;想测试新版 CLI 时在带外重跑此行)
sudo npm install -g @anthropic-ai/claude-code@latest
# 3. LiteLLM 检出。它被 systemd 的 WorkingDirectory 使用,同时是
# .service / .timer 文件的来源。cron 本体则跑在独立的
# ~/litellm-cron-worktree/ 工作树里。
mkdir -p ~/litellm
git clone <litellm 仓库地址> ~/litellm/litellm
git -C ~/litellm/litellm checkout litellm_internal_staging
# 4. gh 认证——该账号必须是 BerriAI/litellm-docs 的 collaborator
gh auth login # 按提示操作;选 HTTPS + token 粘贴流程
# 5. 各家云厂商凭据 + 发布 token
sudo cp ~/litellm/litellm/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.env.example \
/etc/litellm-compat-matrix.env
sudoedit /etc/litellm-compat-matrix.env # 填入真实值
sudo chmod 0600 /etc/litellm-compat-matrix.env
# mateo-berri 的 PAT 单独成文件,通过 systemd LoadCredential 映射进服务,
# 让它不进测试进程的环境(原因见 env.example 注释)。
sudo install -m 0600 /dev/null /etc/litellm-compat-matrix-github-token
sudoedit /etc/litellm-compat-matrix-github-token # 单行内容:PAT
# 6. systemd 单元
sudo cp ~/litellm/litellm/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.service /etc/systemd/system/
sudo cp ~/litellm/litellm/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now litellm-compat-matrix.timer
环境文件需要填哪些凭据
litellm-compat-matrix.env.example 完整定义了各 provider 列对应的凭据变量:
| 变量 | 用途 | 说明 |
|---|---|---|
ANTHROPIC_API_KEY |
anthropic 列 | Anthropic API 直连 |
AWS_BEARER_TOKEN_BEDROCK |
bedrock_invoke / bedrock_converse 列 | 用 Anthropic 的 Bedrock API-key passthrough(长期 bearer token),矩阵不需要 AWS_ACCESS_KEY_ID/SECRET——LiteLLM 的 invoke 与 converse 路由都会读取该变量 |
AWS_REGION_NAME |
Bedrock | 默认 us-east-1 |
VERTEXAI_PROJECT |
vertex_ai / vertex_ai_gpt 列 | GCP VM 上用默认服务账号的 ADC(metadata server),无需 JSON key;若在 GCP 外运行需额外导出 GOOGLE_APPLICATION_CREDENTIALS |
VERTEXAI_LOCATION |
Vertex AI | 默认 global |
AZURE_AI_API_KEY / AZURE_AI_API_BASE |
azure 列 | Azure AI Foundry 上的 Claude 部署 |
OPENAI_API_KEY |
openai GPT 列 | OpenAI 直连 |
AZURE_API_BASE / AZURE_API_KEY |
azure_openai GPT 列 | Azure OpenAI 部署 |
两个可选开关控制"按需开启"的列:COMPAT_MANTLE_CELLS=1 开启 bedrock_mantle 列(需要 AWS 账号启用 Bedrock 上的 Mantle/OpenAI-on-Bedrock 模型,否则该列记 not_tested 而非 fail);COMPAT_OPENAI_GPT_CELLS=1 开启 openai 列(并发 stage 套件下该列常触 CLI 超时,而串行 cron 通常能跑通)。可覆盖项 PROXY_PORT、LITELLM_WORKTREE、DOCS_REPO、DOCS_BRANCH、DOCS_TARGET_PATH(默认 src/data/compatibility-matrix.json)、AUTO_MERGE_METHOD 均有合理默认值。
七、凭据的供应链安全:为什么 PAT 用 LoadCredential 而非环境变量
这套方案最微妙的安全设计在于发布 token 的传递方式。仓库注释解释了完整动机:
套件里有多个单元格会让"模型驱动的 claude CLI"以该用户身份读取任意文件;脚本、pytest、代理的
/proc/<pid>/environ会把环境变量携带的 token 暴露给任何同 UID 的读取者。
因此 PAT 绝不放进 /etc/litellm-compat-matrix.env(该文件内容会落入 pytest、代理、claude CLI 的进程环境)。正确做法是:
- 把 PAT 单独存为
/etc/litellm-compat-matrix-github-token(chmod 0600、单行); - 服务单元用
LoadCredential=github-token:/etc/litellm-compat-matrix-github-token映射; run_daily.sh在启动时读取${CREDENTIALS_DIRECTORY}/github-token(若系统提供了凭据目录),保存为未导出的 shell 变量,每次调用按需传参(GH_TOKEN=...前缀、curl 的Authorization头、push URL),永不进入任何子进程的环境;- 手动运行时可导出
GITHUB_TOKEN,或干脆用SKIP_PUBLISH=1跳过发布。
PAT 所需权限:对 BerriAI/litellm-docs 的写权限——classic repo scope(另有 workflow),或 fine-grained 的 Contents:RW + Pull requests:RW + Workflows:RW。它同时用于四个环节:(a) 解析最新稳定 release、(b) 推送每日矩阵分支、(c) 打开同仓库 PR、(d) 开启 squash 自动合并。
八、日常运维命令
流水线装好后,日常运维只需几条命令:
# 下次什么时候跑?
systemctl list-timers litellm-compat-matrix.timer
# 立即触发一次真实运行(会向 litellm-docs 发 PR)
sudo systemctl start litellm-compat-matrix.service
# 触发一次不开 PR 的运行(非常适合首次验证)
SKIP_PUBLISH=1 ~/litellm/litellm/tests/e2e/claude_code/cron_vm/run_daily.sh
# 调试时收敛到某一个单元格
SKIP_PUBLISH=1 PYTEST_K='basic_messaging_non_streaming and anthropic' \
~/litellm/litellm/tests/e2e/claude_code/cron_vm/run_daily.sh
# 跟踪最近一次运行
journalctl -u litellm-compat-matrix.service -f
# 查看较早的运行日志
journalctl -u litellm-compat-matrix.service --since '2 days ago'
# 暂时停用(例如排障期间)
sudo systemctl disable --now litellm-compat-matrix.timer
注意 SKIP_PUBLISH=1 模式不会发 PR,而是把矩阵 JSON 写到 ~/litellm/litellm/compatibility-matrix.json 供本地检查(见 run_daily.sh)。
九、运维 Gotchas 清单
README 末节列出的是踩坑后的经验沉淀,值得逐条保留:
- venv 钉死 Python 3.12(
CRON_PYTHON_VERSION):e2e 套件使用 PEP 695type别名语法,VM 系统 Python(3.11)无法解析。run_daily.sh用 uv 拉取受管的 CPython 到~/litellm-cron-worktree/.uv-python/并据此同步 venv。版本提升后的首次运行是一次冷 venv 重建。 - 代理端口是 4100 而非 4000:避免开发者 SSH 进同一台 VM 自带
:4000代理时与 cron 冲突。必要时可用 env 文件里的PROXY_PORT=...覆盖。 uv sync --frozen要求解析出的 tag 确实在 GitHub 上打标:若某次正式版发布了但 git tag 没推上去,git checkout一步会失败——补推 tag 后重跑即可。- 发布 token 轮换是你自己的责任:cron 不会刷新 token。
mateo-berri的 PAT 若过期,运行会在git push/gh pr create处以 401 失败("Bad credentials"/"Authentication failed"),需新铸 PAT 更新该文件。 - 升级 Claude Code CLI 后的首次运行风险最高:新版 CLI 若改变线上协议(wire format),矩阵运行可能产生系统性失败。CLI 升级后务必先用
SKIP_PUBLISH=1跑一次,再让下一个定时任务自动触发。 - 磁盘规划:工作树的
.venv约 1.3 GB、.git目录约 1 GB。VM 上至少预留 5 GB 空闲,否则uv sync会在运行中途失败并留下半装态的 venv。
十、可复用性总结
如果把 cron_vm/ 看作一个工程样板,它示范了一套相当完整的"无人值守每日发布"模式,五个设计点可迁移到其他项目:
- 版本双轨制:被测代理(LiteLLM,随每日最新稳定 tag 移动)与被测客户端(Claude Code CLI,人工带外升级)分开管理,只有前者自动化追新。
- 测试代码永远追新:用"开发检出的最新测试 + 持久工作树里打 tag 的稳定产品代码"这一组合,让每个新测试修复在合入当天即进入 cron,而不是等下一个 stable release。
- 结果工件化而非进程状态化:pytest 失败不等于流水线失败——逐测试结果落成 JSON 工件,再经纯函数构建器聚合成"语义化矩阵",把 CI 退出码从"对/错"升级为"绿/红/不适用/未测"四态网格。
- 发布走 PR + 门控自动合并:所有变更都留痕为可浏览的 PR,但干净时由机器人 squash 自动合入;回归时自动扣留合并并显式关闭既有的 auto-merge 请求,人类评审兜底。
- 凭据按敏感度分层:provider 凭据走可被测试进程继承的 EnvironmentFile,发布 token 走 systemd LoadCredential 且仅在需要时以参数形式传递,从架构上避免同 UID 进程从
/proc窃取发布凭据。
对 LiteLLM 仓库而言,这套体系最终服务于一张公开的 Claude Code 兼容性表:它持续回答"当前稳定版 LiteLLM 网关对 Claude Code 各功能 × 各云后端的支持是否完好",并在任何格子在每日刷新中由绿转红的当天,就把带警告横幅的 PR 摆到评审者面前。相关代码均在 cron_vm 与 claude_code e2e 套件 目录下,可继续深入阅读源码与配套单测(如 matrix_builder 单测)做进一步验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00