Per-provider tool-call contract regression sentinel for Chinese LLMs (DeepSeek/Qwen/Kimi/GLM/MiniMax).
Project description
国产模型 tool-call 契约回归哨兵:换 DeepSeek / Qwen / Kimi / GLM / MiniMax 前,CI 先红灯告诉你哪个工具的 schema 不再等价。
把「换模型上线后才发现 function-calling 静默坏掉」提前成一次切换前的
diff。 同一份工具套件分别打两家国产模型的 OpenAI-兼容/chat/completions,把各自的tool_calls归一化成可对账的契约快照,逐字段比对——等价亮绿,不等价亮红并精确指出差异点,drift 时进程非零退出,直接挂进 CI。
ToolDrift 不是「Promptfoo 的中文版」,也不是统一 API / 路由。它接的是一块没人守护的地:DeepSeek-Reasonix 这类绑死单一国产模型的 Coding Agent 很火,但它们结构上不做「换走时 tool-call 契约是否还等价」的交叉校验;通用 eval 框架(Promptfoo)只断言文本输出,刻意不内化任一家的协议怪癖。当 MiniMax-M2 这类被 sermakarevich 反复讨论的 X27 级 agent 模型把「tool-calling 好」当卖点推、而国产模型价格战让「换供应商砍成本」成为每月运维动作时,「换模型 → function-calling 静默坏掉」就从偶发变成系统性风险。ToolDrift 命名并守护「跨国产模型 tool-call 契约等价性」这个新原语——它是绑死方叙事的对偶:站在迁移方,帮你安全地换走。
目录
架构
单进程 CLI,无服务、无数据库、永不代理业务流量——只读各家端点、归一化、比对。
核心原语是 ContractSnapshot:把每家模型吐 tool_calls 的「形状」提炼成一份可 diff 的快照——
ContractSnapshot
├─ provider / model_id # 契约绑定到具体模型/版本
└─ tools: { tool_name -> ToolCallShape }
ToolCallShape
├─ emitted 该工具是否被调出
├─ arg_keys arguments 顶层键集合(排序后)
├─ arg_nesting 每个参数的 JSON 类型/嵌套形状
├─ arguments_encoding object | json_string
├─ parallel_arity 并行 tool_calls 的数量语义
├─ tool_call_id_format openai | custom | absent
└─ finish_reason "tool_calls" vs 其它取值
diff(a, b) 在 tool_name 上对齐两份快照、逐字段判等价,产出 [ToolDelta]——这正是 Promptfoo(断文本)和 DeepSeek-Reasonix(绑死一家)结构上都不会做的那块。
为什么需要它
每家国产模型把 tool_calls 吐得都不一样、且不向后兼容:参数名变了、arguments 从对象变字符串、并行调用的数组语义不同、finish_reason 取值不同。改一行 base_url/model_id、跑通几个聊天 prompt 就上线,结果某个工具的 schema 静默漂移,agent 在生产里调错工具或调不出工具。这是逐模型(per-model)的契约漂移,通用文本 eval 覆盖不到。ToolDrift 把它前移成切换前 CI 里的一盏红绿灯。
安装
pip install tooldrift # 或: uv tool install tooldrift
国内打不开 PyPI?克隆仓库后 pip install -e . 即可(仅 5 个纯 Python 依赖)。
快速开始
零 API key 先看到红绿——所有命令都支持 --from-fixtures,回放 tests/fixtures/ 里的离线样本:
tooldrift snapshot --base deepseek --from-fixtures # 抓一份契约快照
tooldrift run --old deepseek --new qwen --from-fixtures # 跨两家比对,drift 时非零退出
echo "CI exit code: $?" # → 1(捕获到漂移)
样例输出(DeepSeek → Qwen 切换捕获到 2 处契约漂移)
ToolDrift deepseek/deepseek-chat → qwen/qwen-plus suite=weather
✗ get_forecast contract drift
arg_keys days, include, location, unit → days, location
arg_nesting:days integer → string
arguments_encoding json_string → object
tool_call_id_format openai → custom
finish_reason tool_calls → stop
✗ get_weather contract drift
arguments_encoding json_string → object
tool_call_id_format openai → custom
finish_reason tool_calls → stop
╭──────────────────────────────────────────────────╮
│ FAIL — BREAKING drift in 2 of 2 tool(s). Exit 1. │
╰──────────────────────────────────────────────────╯
接真实端点:把各家 key 写进环境变量(DEEPSEEK_API_KEY、DASHSCOPE_API_KEY…,见 examples/contract.yaml),去掉 --from-fixtures 即可。
用法
四个子命令,对应 OSS 核心:
# 1) snapshot —— 抓一家的契约快照,落 JSON(契约绑定到具体版本)
tooldrift snapshot --base deepseek --suite examples/suite.weather.yaml -o snapshots/deepseek.json
# 2) diff —— 纯离线比对两份已落地的快照(无网络),drift 即非零退出
tooldrift diff snapshots/deepseek.json snapshots/qwen.json
# 3) run —— 一行式 CI 入口:探测 old vs new、比对、红绿报告 + 退出码
tooldrift run --old deepseek --new qwen --suite examples/suite.weather.yaml
# 4) compare-table —— 跨五家产出可传播的 Markdown 对比表
tooldrift compare-table --from-fixtures -o COMPARISON.md
更多示例见 examples/。把第 3 行直接写进 CI(见下方路线图里的 .github/workflows/tooldrift.yml),换模型 PR 上 schema 不等价就 fail。
Demo
同样的 30 秒流程也有 asciinema 录像:
assets/demo.cast。
五家 tool_calls 对比表
tooldrift compare-table 跑一次的副产品——这张表本身就是最好的传播钩子(下表为离线 fixture 实测,标 Δ 的行即跨家不等价点):
tool_calls contract comparison — suite weather
| tool | field | deepseek | qwen |
|---|---|---|---|
| get_forecast | emitted | ✓ | ✓ |
| Δ arg_keys | days, include, location, unit |
days, location |
|
| Δ args_encoding | json_string |
object |
|
| parallel_arity | 1 | 1 | |
| Δ id_format | openai |
custom |
|
| Δ finish_reason | tool_calls |
stop |
|
| get_weather | emitted | ✓ | ✓ |
| arg_keys | location, unit |
location, unit |
|
| Δ args_encoding | json_string |
object |
|
| parallel_arity | 1 | 1 | |
| Δ id_format | openai |
custom |
|
| Δ finish_reason | tool_calls |
stop |
接上五家真实 key 后,
tooldrift compare-table会把 kimi / glm / minimax 三列也填满。
配置
contract.yaml 顶层键(完整示例见 examples/contract.yaml):
| 键 | 类型 | 默认 | 含义 |
|---|---|---|---|
version |
int | 1 |
契约文件格式版本 |
suite |
path | — | 引用的工具套件 YAML(prompt + 工具定义) |
providers |
map | — | 受测 provider 列表:每家 base_url / model_id / api_key_env |
providers.<p>.api_key_env |
str | — | 读 key 的环境变量名——key 从不写进文件或快照 |
expected |
map | (可选) | 钉死一份「已知良好」契约,让 run 回归每家是否仍满足它 |
付费层 · 托管契约漂移看板
OSS 核心(snapshot / diff / run / compare-table CLI)永久免费,护城河是开放的契约快照格式。商业层是托管的「契约漂移看板」——持续对五家最新 API 定时回归,新版本一发就推送告警(如「GLM 又改了 arguments 字符串化」),按团队订阅:
| 档位 | 价格 | 内容 |
|---|---|---|
| Team | ¥499/月 | 托管定时回归 + 五家变更告警(邮件 / 飞书 / 钉钉 webhook)+ 私有 contract 托管 |
| Enterprise | ¥2,999/月起 | 私有部署、报告留痕(信创 / 政企合规交付)、五家之外按需适配(豆包 / 百川) |
首付费客户来自承诺「支持多家国产模型」的 agent 中间件 / 框架团队——他们每接一家新模型都是盲跳,最有动机为现成等价性测试 + 协议变更订阅付费。看板本身不在本仓库范围内(CLI 已埋好开关与文档接缝);想试用托管层请提 issue 联系。
路线图
- m1 ·
snapshot探测一家、归一化ContractSnapshot、落 JSON - m2 ·
diff纯函数 +run红绿报告 + CI 非零退出码 - m3 ·
compare-table跨五家产出对比 Markdown 表 - m4 · CI 模板(
.github/workflows/tooldrift.yml)+ demo + 双语 polished README + 货币化接缝 - 流式 tool-call(SSE delta)重组
- 五家之外的模型适配(豆包 / 百川…,付费按需)
- 托管「契约漂移看板」SaaS(付费层)
对比 DeepSeek-Reasonix
诚实定位——ToolDrift 站在迁移方,是绑死方的对偶,不踩对方赛道:
| 维度 | ToolDrift | DeepSeek-Reasonix |
|---|---|---|
| 目标 | 跨国产模型 tool-call 契约等价性回归 | 围绕 DeepSeek 一家把 agent 工程化到极致 |
| 单模型深度 / prefix-cache 工程 | — | ✓(这是它 2.5 万星的护城河) |
| 跨模型迁移安全(换走时是否等价) | ✓ | —(结构上不做,做了会消解 DeepSeek-native 卖点) |
| 即开即用的 agent 终端体验 | partial(CLI 工具,非 agent) | ✓ |
| 进 CI 的红绿退出码 | ✓ | — |
License
MIT。欢迎提 issue 描述你的真实迁移场景,或 PR 贡献一家新 provider 的适配。
Share this
ToolDrift — 国产模型 tool-call 契约回归哨兵。换 DeepSeek/Qwen/Kimi/GLM/MiniMax 前,
CI 先红灯告诉你哪个工具 schema 不再等价。Agent 迁移方的对偶。 https://github.com/SuperMarioYL/tooldrift
MIT © 2026 SuperMarioYL
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tooldrift-0.1.0.tar.gz.
File metadata
- Download URL: tooldrift-0.1.0.tar.gz
- Upload date:
- Size: 26.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
243d6ee9ccb1307a8472de917eaf6a8ef970d314dbfe7abe8427426ec29b4449
|
|
| MD5 |
8533491275c57877f6659c7d1af94f65
|
|
| BLAKE2b-256 |
d53095d86aed83fefa14dfca41d9a050645459da27b9a15540d1ec007431af08
|
Provenance
The following attestation bundles were made for tooldrift-0.1.0.tar.gz:
Publisher:
release.yml on SuperMarioYL/tooldrift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tooldrift-0.1.0.tar.gz -
Subject digest:
243d6ee9ccb1307a8472de917eaf6a8ef970d314dbfe7abe8427426ec29b4449 - Sigstore transparency entry: 2002704634
- Sigstore integration time:
-
Permalink:
SuperMarioYL/tooldrift@b8518ea793ef7346fc30d2d75929b843ac59d674 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/SuperMarioYL
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b8518ea793ef7346fc30d2d75929b843ac59d674 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tooldrift-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tooldrift-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e449884f990f166c8176e6ee009ea88795ae47dee13d56ef7c4f9f9c13fac650
|
|
| MD5 |
ff3dda183890c97e65a30c7aa9b49c34
|
|
| BLAKE2b-256 |
4d0144e5a6cee01ba02e374d4e2bcce154fa0bb50563abceecdf0544dbf20a25
|
Provenance
The following attestation bundles were made for tooldrift-0.1.0-py3-none-any.whl:
Publisher:
release.yml on SuperMarioYL/tooldrift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tooldrift-0.1.0-py3-none-any.whl -
Subject digest:
e449884f990f166c8176e6ee009ea88795ae47dee13d56ef7c4f9f9c13fac650 - Sigstore transparency entry: 2002704712
- Sigstore integration time:
-
Permalink:
SuperMarioYL/tooldrift@b8518ea793ef7346fc30d2d75929b843ac59d674 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/SuperMarioYL
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b8518ea793ef7346fc30d2d75929b843ac59d674 -
Trigger Event:
push
-
Statement type: