caveman SDK Parity 契约:一份 fixtures.json 如何成为 TS 与 Python 双 SDK 的发布门禁
packages/sdk/parity 目录是 caveman 仓库中跨语言 SDK 一致性(parity)测试的核心:一个语言中立的 fixtures.json 被 TypeScript(@caveman-ai/sdk)和 Python(caveman_cloud)两个 SDK 各自完整执行一遍。它的定位不是文档,而是发布门禁(release gate)——只要某个字段在一个 SDK 中存在而在另一个 SDK 中缺失,CI 上必然有一侧变红。读完后你将理解:parity 契约文件的完整结构(config、命名 header 集、operations 列表)、两侧测试如何 mock 传输层并断言精确的 wire 请求与派生结果、以及维护双语言 SDK 时必须遵守的五条编辑规则。
1. 定位:release gate,不是文档
目录说明(CLAUDE.md,其内容与 AGENTS.md 一致)开篇即给出这条契约的"承重诚实属性"(load-bearing honesty property):
线协议契约只有一个东西,用两种语言表达(
@caveman-ai/sdkTS +caveman_cloudPython)。一个字段在一个 SDK 中存在、在另一个中缺失,就会使某一侧的断言失败——因此这个文件夹是发布门禁,而不是文档。
目录布局非常简单:
- fixtures.json —— 契约本体(当前含 29 个 operation,
version: 1); - runtime-policy.fixtures.json —— 运行策略客户端的配套共享 fixture(由两侧 runtime-policy 测试驱动,不在本文展开)。
两侧的驱动器:
| 语言 | 驱动文件 | mock 对象 |
|---|---|---|
| TypeScript | parity.runtime.mjs | 全局 fetch |
| Python | test_parity.py | urllib.request.urlopen |
每一侧都实现了 per-operation handler:执行真实的 SDK 调用、捕获 wire 请求、把结果规范化为 canonical snake-key 值。两侧都遍历每一个 operation——缺少 handler 是失败(failure),绝不跳过(never a skip)。
2. fixtures.json 的结构:config + 命名 header 集 + operations
契约文件顶部是一个固定的 config(即"一个 Cave",两侧 SDK 客户端都从它构造):
{
"api_key": "cave_live_parity_key",
"base_url": "http://gateway.test",
"control_url": "http://control.test",
"agent": "parity-agent",
"default_workflow": "parity-workflow",
"retention": "metadata",
"user": "parity-user-hash"
}
接着是命名 header 集,供各 operation 的 expect.wire.headers 以字符串引用(TS 侧在 fixtures[op.expect.wire.headers] 解引用,Python 侧同理)。契约文档明确列出 std_headers / std_headers_traced / std_headers_async_traced / otlp_headers 四组;实际 fixture 中还存在第五组 std_headers_artifact_traced(artifact 上报操作使用),其差异点是额外携带 x-cave-artifact-envelope: value-v1 与 x-cave-artifact-source: tool:fetch。各组语义:
std_headers—— 基础六件套:content-type、authorization: Bearer …、x-cave-agent、x-cave-workflow、x-cave-retention、x-cave-user-hash;std_headers_traced—— 在基础集上追加x-cave-trace-id: aaaabbbbccccddddeeeeffff00001111与x-cave-parent-span-id: 1122334455667788;std_headers_async_traced—— traced 集上再追加x-cave-async: true;std_headers_artifact_traced—— traced 集上追加 artifact envelope/source 两键;otlp_headers—— OTLP 导出专用:content-type+x-cave-api-key(而非 Bearer)+ agent/workflow/retention/user-hash。
注意 trace id / span id 是硬编码常量而非 SDK 生成的值——这是"无任何随机值进入断言"规则的体现,见第 5 节。
3. 单个 operation 的解剖
每个 operation 是一个对象,包含四部分:name(handler 映射键)、input(喂给真实 SDK 调用的参数)、response(mock 传输层返回的罐头响应)或 transport: "error"(强制字节安全直通),以及 expect 块。expect 又分 wire 与结果两部分:
expect.wire:method、path、headers(命名集或字面量对象),body 二选一——精确的body或仅校验键集合的body_keys;另可选base: "control"表示请求发往control_url而非base_url;expect.result:期望的派生结果(canonical snake-key 值),或result_from: "response"表示结果必须逐字节等于罐头响应。
以 tool_search 为例(取自 fixtures.json 第 168–217 行):
{
"name": "tool_search",
"input": {
"catalog": [
{ "name": "search", "description": "Search things", "input_schema": { "type": "object", "properties": {} }, "read_only": true, "idempotent": true, "always_load": false },
{ "name": "fetch", "description": "Fetch a thing", "input_schema": { "type": "object", "properties": {} }, "read_only": false, "idempotent": false, "always_load": true }
],
"query": "find a thing",
"context": "parity test",
"max_tools": 5,
"session_id": "tool-session-1"
},
"response": {
"session_id": "tool-session-1",
"tools": [{ "name": "search", "description": "Search things" }],
"sent_schema_tokens": 120,
"full_schema_tokens": 840,
"deferred_count": 1,
"token_basis": "estimated_bytes_div_4",
"method": "lexical-hit-rate"
},
"expect": {
"wire": {
"method": "POST",
"path": "/sdk/v1/tool-search",
"headers": "std_headers",
"body": { "tools": [ ... ], "query": "find a thing", "context": "parity test", "max_tools": 5, "session_id": "tool-session-1" }
},
"result": {
"sent_schema_tokens": 120,
"full_schema_tokens": 840,
"deferred_count": 1,
"method": "lexical-hit-rate",
"token_basis": "estimated_bytes_div_4",
"basis": "inferred",
"session_id": "tool-session-1",
"saved_tokens": 720,
"reduction_pct": 85.7,
"tool_count": 1
}
}
}
这里同时验证了三件事:请求体与 input 语义一致地落到 wire 上;派生值(saved_tokens: 840-120=720、reduction_pct: 85.7)由 SDK 本地计算且必须精确相等;结果被规范化为 snake-key。
4. 29 个 operation 覆盖的 SDK 面
当前 fixture 的 operations 列表按能力域覆盖如下(顺序即文件内顺序):
| 能力域 | operation 名 | 断言重点 |
|---|---|---|
| 上下文装配 | context_assemble / context_assemble_self / context_assemble_none |
纯本地:request(model/tools/system/messages)、x-cave-assembly header(v1;slots=4;prefix=15506e067a67;vbb=1)、prefix_hash(64 位 hex)、breakpoints、stable_tokens: 56;emit_cache_hints: "self" 变体额外在 tools[0] 上产出 cache_control: {type: "ephemeral"} 并记录 breakpoint |
| 工具检索 | tool_search / tool_search_embeddings / tools_builder_search |
POST /sdk/v1/tool-search;embeddings ranker 原样透传;builder 变体额外返回 strategy 与 initial_tool_names |
| 压缩 | compress / compress_toon / compress_passthrough / compress_bad_report / compress_optimistic_ratio / compress_unchanged_false_claim |
POST /sdk/v1/compress;transport: "error" 与坏报告(tokens_before: "not-a-number")都触发字节安全直通:输出=原输入、ratio: 0.0、token_count_basis: "unavailable";乐观 ratio(服务端报 0.9 但 before/after 不符)被纠正为 0.2 |
| 优化计划 | cave_plan |
GET /sdk/v1/cave-plan,走 otlp_headers,计划逐字节透传(result_from: "response") |
| 检查点 | checkpoint / checkpoint_expand |
POST /sdk/v1/checkpoints(body 含 workflow 注入);expand 路径做 URL 编码:/sdk/v1/checkpoints/tenant%20a%2Fref%2B1/expand |
| 上下文打包 | context_pack |
POST /sdk/v1/context/pack;deferred_ids: ["billing"] 精确断言被挤出的项 |
| 工具事件 | event_tool_call |
只断言 body_keys(duration_ms、name、options、outcome、sequence、span_type、tags、workflow),容忍服务端补字段 |
| 工件 | artifacts_page / artifacts_get |
page 返回定格式字符串:[cave-artifact id=art_123 source=tool:fetch type=application/json] … [/cave-artifact] |
| 模型调用 | model_create_async / model_create_traced / provider_create_untraced |
同样 POST /openai/v1/responses:异步变体带 x-cave-async: true,traced 变体带 trace 双头,裸 cave.openai() 变体不带任何连续性 id |
| Bedrock 路由 | bedrock / bedrock_mantle |
无网络的路由描述符:默认 gateway_prefix: /bedrock,mantle 为 /bedrock/anthropic,instrumented: true、sdk_only: false |
| OTLP 导出 | otlp_export / otlp_export_traced |
POST /v1/traces;完整断言 resourceSpans 结构(service.name、cave.agent 资源属性,gen_ai.* span 属性,纳秒时间戳为字符串,kind: 3,status.code: 1);traced 变体把 parentSpanId 钉在注入的 root span 上 |
| 预留能力 | jobs_unavailable |
cave.jobs.submit 本地失败,error_code: cave_async_jobs_unavailable,不发任何网络请求 |
| 循环熔断 | retry_loop_breaker / retry_loop_breaker_key_order |
RetryLoopBreaker 在第 2 次相同调用触发;key order 变体断言 {a:1,b:2} 与 {b:2,a:1} 视为同一调用 |
其中 context_assemble* 与 bedrock*、jobs_unavailable、retry_loop_breaker* 不带 expect.wire,断言逻辑会转而要求 captured.length == 0——纯本地 API 也必须是"零 wire 请求",防止误发网络调用。
5. 如何强制:两侧驱动的断言流水线
TypeScript 侧(parity.runtime.mjs,用 node --test 运行,构建后 import 自 dist/):
installMock(op)替换globalThis.fetch:捕获{url, method, headers, body};op.transport === "error"时抛simulated transport error,否则返回罐头op.response;for (const op of fixtures.operations)为每个 operation 生成一个test("parity: " + op.name);handlers[op.name]不存在即断言失败,错误信息为 "the SDK is missing this capability";- header 断言前用
lowerKeys()把实际请求头转小写,与 fixture 的命名集deepStrictEqual——因为 Python 的urllib会首字母大写化 header,跨语言比较只能比"键小写后的集合 + 值"; - body 有
body时做deepStrictEqual精确比对,只有body_keys时按排序后的键集合比对; - 结果断言:
result_from === "response"取罐头响应,否则取expect.result,与实际结果deepStrictEqual。
Python 侧(test_parity.py)结构完全对称:@pytest.mark.parametrize("op", OPS) 逐条驱动;patch("urllib.request.urlopen", side_effect=fake_urlopen) 捕获请求并在 transport == "error" 时抛 urllib.error.URLError;HANDLERS 字典与 TS 一一对应;每个 handler 把 camel/对象结果显式映射回 canonical snake-key 字典再与期望比较。Python 侧还有一条额外守卫 test_tool_events_are_traced_while_bare_provider_calls_stay_untraced:直接从 fixture 断言 event_tool_call 用 std_headers_traced 而 provider_create_untraced 用 std_headers——把"trace 内带连续性头、裸 provider 调用不带"这条契约也钉死在数据上。
两侧各有一个防退化守卫测试(parity: fixtures cover the documented operations / test_parity_fixtures_cover_surface):operations 长度必须 ≥ 10,且每个 operation 都必须有 handler,防止有人用空 fixture 或短 fixture 让门禁"静默通过"。TS 侧还额外验证 bedrock 描述符对未知 endpoint 抛 /runtime or mantle/ 错误。
6. 五条编辑规则(来自契约文档,必须遵守)
CLAUDE.md 的 "Editing rules" 一节给出了维护这份契约的操作规程,逐条展开:
- 单边改字段/方法 → 先加 operation(或 body key)。加到 fixture 后,另一个 SDK 的那一侧会立刻变红,直到它对齐为止——"That red is the point."(红就是目的)。也就是说,变更流程是 fixture 先行,红 CI 驱动另一侧实现。
- Header 按键小写比较(Python 的
urllib会把 header 首字母大写化);跨语言必须匹配的是键集合 + 值,而不是键的原始大小写。 - 只用两种语言编码一致的取值。文档明确点名 expand 路径中避开
! ( ) *:JS 的encodeURIComponent与 Python 的quote对这几个字符的百分号编码行为不一致。这正是checkpoint_expand用例只使用空格、/、+(编码为%20、%2F、%2B,两者行为一致)的原因。 - 任何随机值不得进入断言。SDK 本来会自己铸造的 id(trace id、span id)全部通过 operation 的
input注入,并钉死在期望 header 集里——otlp_export钉 span id 用的是同一套手法。 - 运行方式:文档给出的命令是
make product-test PRODUCT=sdk-ts&&make product-test PRODUCT=sdk-python(当前仓库根目录未包含 Makefile,该命令应由外层构建环境提供)。不依赖外层 make 时,两侧测试文件头部给出了直跑方式:TS 为pnpm build后node --test tests/parity.runtime.mjs,Python 为在本目录直接pytest(要求 Python ≥ 3.13,dependencies = [],见 pyproject.toml)。
7. 与两个 SDK 文档的关系:镜像约定是双向的
parity 契约并不是孤立存在的——它只是把两个 SDK 文档中"mirror"约定机制化:
- TS 侧(CLAUDE.md)的 Gotchas 写着 "mirror sdk-python: every field/method exists in both, enforced by the shared parity suite — a divergence is a CI failure, not a convention slip. Change one SDK, change both and the fixtures";
- Python 侧(CLAUDE.md)对称写着 "sdk-python and sdk-ts mirror the same field names and /sdk/v1/* contract — enforced by the shared parity suite … A divergence is a CI failure"。
两侧文档还把若干具体契约与 fixture 对应起来:x-cave-workflow 永远不省略(缺省 unlabeled-workflow)、延迟工具检索的 session_id / x-cave-tool-session 交接("update sdk-python + parity fixtures with any change")、reduction_pct 保留一位小数且 saved_tokens 是本地推导值等。理解这些对应关系后,parity 目录的实际角色就很清楚:它是两份 SDK 文档共享事实(shared truth)的唯一数据源,测试代码只负责执行与断言,而契约本身是可读、可 diff、可审计的 JSON。
8. 小结:从契约到门禁的最小闭环
把整套机制压缩成一句话:一个 JSON 文件 + 两份逐条执行它的测试。新增能力时的标准动作是:在 fixtures.json 加 operation → 在 parity.runtime.mjs 与 test_parity.py 各加 handler → 跑两侧测试,直到都绿。这套设计对任何维护多语言 SDK 的团队都有直接参考价值:用"共享 fixture + 强制全覆盖(缺 handler 即失败)+ 随机值注入 + 编码一致性约束"四件事,就能让"两个语言的 SDK 行为完全一致"从口头约定变成 CI 里可验证的事实。
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 StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
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