Caveman SDK Parity 机制:用一份共享 fixtures 把 TypeScript 与 Python SDK 焊成同一份线协议
packages/sdk/parity/ 是 Caveman 项目中 TypeScript SDK(@caveman-ai/sdk)与 Python SDK(caveman_cloud)之间的跨语言一致性契约目录:一份语言中立的 fixtures 文件被两个 SDK 的测试各自完整执行,任何一个字段在一侧存在、另一侧缺失,都会让对应那一侧的断言失败——因此它本质上是发布门禁(release gate),而不是文档。读完本文,你将理解这份契约文件的完整结构、双侧测试 harness 如何逐操作校验"线上请求 + 派生结果",以及修改任一侧 SDK 时必须遵守的编辑规则与 CI 运行方式。
一、Parity 目录的定位:一份契约,两种表达
项目的双 SDK 共享同一个网关(gateway),其线协议(wire contract)只有一份,但用两种语言各表达一次。parity/AGENTS.md 对这一点给出了原话式的定义:
"One language-neutral fixture file, run by both SDKs. The load-bearing honesty property of the SDKs: the wire contract is one thing, expressed twice."
目录内容非常克制,只有三个文件:
- packages/sdk/parity/fixtures.json — 契约本体;
- packages/sdk/parity/AGENTS.md(与 CLAUDE.md 内容相同)— 本目录的编辑规则说明;
- packages/sdk/parity/runtime-policy.fixtures.json — 运行时策略客户端的配套 fixtures(由两侧 runtime-policy 测试驱动,不在本文主题范围内,仅作旁证存在)。
其"诚实属性"的机制很直白:把同一份操作列表分别喂给 TS 和 Python 两个真实 SDK 客户端,在 mock 传输层上捕获真实发出的请求,再与预期逐字段比对。一侧实现落后或超前,CI 变红——"That red is the point"(变红本身就是目的)。
二、fixtures.json:契约文件的完整结构
2.1 顶层布局
fixtures.json 顶层由五部分组成:
{
"version": 1,
"description": "Cross-language SDK parity fixtures. ... change one SDK without the other and CI goes red. Both SDKs MUST implement a handler for every operation; a missing handler is a failure, never a skip.",
"config": {
"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"
},
"std_headers": { ... },
"std_headers_traced": { ... },
"std_headers_artifact_traced": { ... },
"std_headers_async_traced": { ... },
"otlp_headers": { ... },
"operations": [ ... ]
}
其中:
config描述"一个 Cave"(客户端配置):api_key、base_url、control_url、agent、default_workflow、retention、user七个字段,两个 SDK 的 harness 用它分别构造new Cave({...})(TS)与Cave(api_key=..., base_url=..., ...)(Python);- 命名 header 集合 供
operations[].expect.wire.headers按名字引用(见 2.2 节); operations是一个有序列表,每个操作是一条完整的"输入 → 预期线请求 → 预期派生结果"三元组。
description 字段本身就是一份可执行声明:两个 SDK 必须为每一个操作实现 handler,缺失 handler 是失败而非跳过;改了某一个 SDK 而没改另一个,CI 变红。
2.2 命名 Header 集合
AGENTS.md 文档列出四个命名 header 集合(std_headers / std_headers_traced / std_headers_async_traced / otlp_headers),而 fixtures 实际还包含第五个 std_headers_artifact_traced,供 artifacts_page 操作使用。各集合的语义如下表:
| 集合名 | 相对基础集合的增量 | 使用的操作 |
|---|---|---|
std_headers |
content-type、authorization: Bearer ...、x-cave-agent、x-cave-workflow、x-cave-retention、x-cave-user-hash |
所有普通网关调用(tool-search、compress、context/pack、artifacts get 等) |
std_headers_traced |
增加 x-cave-trace-id: aaaabbbbccccddddeeeeffff00001111、x-cave-parent-span-id: 1122334455667788 |
经 trace 发起的调用(checkpoint、events、artifacts、model create、checkpoint expand) |
std_headers_artifact_traced |
traced 基础上再增加 x-cave-artifact-envelope: value-v1、x-cave-artifact-source: tool:fetch |
artifacts_page |
std_headers_async_traced |
traced 基础上增加 x-cave-async: true |
model_create_async(设置了 latency_class 的模型调用) |
otlp_headers |
不含 authorization,改用 x-cave-api-key |
cave_plan(GET /sdk/v1/cave-plan)与 otlp_export*(POST /v1/traces) |
这些集合把"哪些请求必须携带 trace 连续性 id、哪些不带"固化成了断言对象:例如"直接挂在 Cave 上(而非 trace 内)的 provider 调用必须不带 x-cave-trace-id"这条规则,就是通过 provider_create_untraced 操作引用 std_headers 而非 std_headers_traced 来锁定的。
2.3 单个 Operation 的结构
每个操作是一个 name + input + 可选 response / transport + expect 的对象:
input:该操作的输入参数(snake_case,与网关线字段一致);SDK 内部需要生成的 id(trace id、span id)也在这里显式注入(见 4.4 节);response:预制(canned)的网关响应;若改为"transport": "error",则 mock 传输层直接抛错,用于强制 SDK 走字节安全的直通(byte-safe pass-through)分支;expect.wire:对线上请求的断言,含method、path、headers(命名集合名或字面量对象)、以及二选一的body(完整精确匹配)或body_keys(只断言键集合)。部分操作可加base: "control",表示请求应发往control_url而非base_url;expect.result:对 SDK 派生结果的断言(规范化的 snake 键值对象);或expect.result_from: "response",表示"SDK 必须逐字节透传预制响应"(如cave_plan、otlp_export、model_create_*这类纯透传操作)。
AGENTS.md 对 layout 的原始描述与此一致:"Each operation carries its input, a canned response (or transport: \"error\" to force a byte-safe pass-through), and an expect block: wire ... plus a result (or result_from: \"response\")."
2.4 操作全集:25 个操作覆盖 SDK 表面
当前 fixtures 包含 25 个操作(harness 中还有"操作数 ≥ 10"的保底断言,防止 fixture 文件被清空后悄悄通过门禁)。按功能分组:
| 分组 | 操作名 | 锁定的契约点 |
|---|---|---|
| 上下文组装(本地) | context_assemble、context_assemble_self、context_assemble_none |
emit_cache_hints 的 gateway/self/none 三态对 cache_control 断点的影响、x-cave-assembly 头、prefix_hash、stable_tokens |
| 工具检索 | tool_search、tool_search_embeddings、tools_builder_search |
POST /sdk/v1/tool-search 线请求体、ranker 透传、saved_tokens/reduction_pct 派生、always_load 工具的初始集 |
| 压缩 | compress、compress_toon、compress_passthrough、compress_bad_report、compress_optimistic_ratio、compress_unchanged_false_claim |
POST /sdk/v1/compress、传输失败/坏报告/虚报压缩率的降级行为(见 4.3 节) |
| 节省计划 | cave_plan |
GET /sdk/v1/cave-plan、otlp_headers、响应逐字透传 |
| 检查点 | checkpoint、checkpoint_expand |
POST /sdk/v1/checkpoints、GET /sdk/v1/checkpoints/{ref}/expand 的 URL 编码 |
| 上下文打包 | context_pack |
POST /sdk/v1/context/pack 的 items/options 原样上送、deferred_ids 返回 |
| 工件 | artifacts_page、artifacts_get |
POST /sdk/v1/artifacts 的 workflow 注入与 [cave-artifact] 信封、GET /sdk/v1/artifacts/{id} |
| 工具事件 | event_tool_call |
POST /sdk/v1/events 的 body 键集合(body_keys 断言) |
| 模型调用 | model_create_async、model_create_traced、provider_create_untraced |
x-cave-async 头、trace 头有无的组合(对应 2.2 节三个集合) |
| Bedrock 路由 | bedrock、bedrock_mantle |
无网络的路由描述符:/bedrock 与 /bedrock/anthropic 前缀、sdk_only 字段 |
| OTLP 导出 | otlp_export、otlp_export_traced |
POST /v1/traces 的完整 OTLP/JSON body(gen_ai.* 属性映射、span id 固定) |
| 异步作业(占位) | jobs_unavailable |
本地即失败 cave_async_jobs_unavailable,不发任何网络请求 |
| 重试熔断 | retry_loop_breaker、retry_loop_breaker_key_order |
连续相同工具调用计数、参数键序无关的比较 |
三、双侧 Harness:TS 与 Python 如何强制执行
3.1 TypeScript 半边
TS 半边位于 packages/sdk/typescript/tests/parity.runtime.mjs,它 mock globalThis.fetch(文档中的相对路径 ../typescript/tests/parity.runtime.mjs 即此文件),核心流程:
- 读取
packages/sdk/parity/fixtures.json,用config构造真实的Cave实例; - 每个 fixture 操作对应一个 handler,handler 执行真实的 SDK 调用(
cave.toolSearch、cave.compress、cave.trace(...)回调等),并把 camelCase 结果字段映射回规范 snake 键(如sentSchemaTokens→sent_schema_tokens、gatewayPrefix→gateway_prefix); - mock fetch 捕获
{ url, method, headers, body };若op.transport === "error"则抛错; - 逐操作驱动:
for (const op of fixtures.operations) {
test(`parity: ${op.name}`, async () => {
const handler = handlers[op.name];
assert.ok(handler, `no TS parity handler for operation "${op.name}" — the SDK is missing this capability`);
installMock(op);
const actual = await handler(makeCave(), op.input);
if (op.expect.wire) {
assert.equal(captured.length, 1, ...); // 必须恰好一次线请求
assert.equal(req.url, base + op.expect.wire.path, ...);
assert.equal(req.method, op.expect.wire.method, ...);
assert.deepStrictEqual(lowerKeys(req.headers), expHeaders, ...); // 键小写后比对
if (op.expect.wire.body !== undefined) {
assert.deepStrictEqual(req.body, op.expect.wire.body, ...);
} else if (op.expect.wire.body_keys !== undefined) {
assert.deepStrictEqual(Object.keys(req.body ?? {}).sort(),
[...op.expect.wire.body_keys].sort(), ...);
}
} else {
assert.equal(captured.length, 0, ...); // 本地操作必须零网络
}
const expected = op.expect.result_from === "response" ? op.response : op.expect.result;
assert.deepStrictEqual(actual, expected, ...);
});
}
值得注意的两个细节:
- URL 断言的是完整地址:
base + path,其中base按wire.base === "control"在control_url与base_url之间选择——这锁定了"哪些端点走 control 通道"; - 文件尾部还有两条守卫测试:
parity: fixtures cover the documented operations断言操作数 ≥ 10 且每个操作都有 handler("Guard against an empty/short fixture file silently passing the gate");另外bedrock descriptor rejects an unknown endpoint验证描述符对未知endpoint抛出/runtime or mantle/错误。
3.2 Python 半边
Python 半边位于 packages/sdk/python/tests/test_parity.py,mock 对象是 urllib.request.urlopen(Python SDK 为纯标准库实现,无任何第三方依赖)。结构与 TS 半边严格镜像:
@pytest.mark.parametrize("op", OPS, ids=[o["name"] for o in OPS])
def test_parity(op: dict[str, Any]) -> None:
handler = HANDLERS.get(op["name"])
assert handler is not None, f'no Python parity handler for operation "{op["name"]}" — the SDK is missing this capability'
...
with patch("urllib.request.urlopen", side_effect=fake_urlopen):
actual = handler(_make_cave(), op["input"])
...
expected = op["response"] if op["expect"].get("result_from") == "response" else op["expect"].get("result")
assert actual == expected, f'{op["name"]}: result'
fake_urlopen 捕获 req.full_url、req.get_method()、键小写化后的 header({k.lower(): v for k, v in dict(req.headers).items()})以及 JSON 解析后的 body;transport == "error" 时抛 urllib.error.URLError。同样的"操作数 ≥ 10 + 全量 handler 覆盖"守卫也存在于 test_parity_fixtures_cover_surface。此外还有一条元测试 test_tool_events_are_traced_while_bare_provider_calls_stay_untraced,直接从 fixtures 断言 event_tool_call 必须引用 std_headers_traced 而 provider_create_untraced 必须引用 std_headers——把"trace 连续性规则"本身也纳入了契约。
3.3 双侧共同的判定语义
综合两个 harness,parity 的判定规则可以概括为四句话(与 AGENTS.md "How it's enforced" 一节完全对应):
- 执行真实调用:handler 不是重复实现一遍协议,而是调用 SDK 公共 API 并捕获其真实输出,因此它同时校验"线请求"和"派生结果"两层;
- 结果规范化:两侧各自把语言特有命名(camelCase / snake_case)映射到同一套 canonical snake 键值再比较,避免命名风格差异污染断言;
- 全量遍历:两侧都遍历每一个操作,"a missing handler is a failure, never a skip";
- 零网络:TS mock
fetch、Python mockurlopen,测试永远不打真实网络。
四、从典型操作看契约锁定的细节
4.1 context_assemble 家族:缓存断点的三态
context_assemble、context_assemble_self、context_assemble_none 三个操作共享同一组 slots(一个 volatile 的 turn、一个 stable 的 tools、一个 stable 的 system、一个 session 级的 playbook),只改变 emit_cache_hints(缺省 / "self" / "none"),断言派生结果的三个维度:
"headers": { "x-cave-assembly": "v1;slots=4;prefix=15506e067a67;vbb=1" },
"prefix_hash": "15506e067a6722f449496ebe3eed6f340bcfefc53483026603d0b1d97b16eced",
"breakpoints": ["tools[0]"], // 仅 emit_cache_hints="self" 时非空
"stable_tokens": 56,
"token_basis": "estimated_bytes_div_4",
"basis": "inferred",
"volatile_below_breakpoint": true
emit_cache_hints: "self"时,tools[0]获得cache_control: {"type": "ephemeral"},breakpoints为["tools[0]"];- 其余两态
breakpoints为空数组; prefix_hash前 12 位与x-cave-assembly头中的prefix=15506e067a67保持一致,锁定了"头里的短前缀就是完整哈希前缀"这一约定;- 该操作没有
wire段(纯本地组装、零网络),走的是 harness 中"captured.length 必须为 0"的分支。
4.2 tool_search:派生字段的精确算术
tool_search 同时断言线请求与派生结果:
- wire:
POST /sdk/v1/tool-search,std_headers,body 与input的tools/query/context/max_tools/session_id逐字段一致(camelCase 的inputSchema在 wire 上是input_schema); - result:在透传网关的
sent_schema_tokens: 120、full_schema_tokens: 840、deferred_count: 1、method: "lexical-hit-rate"之外,额外断言 SDK 本地派生的字段:saved_tokens: 720(= 840 − 120)、reduction_pct: 85.7(保留一位小数)、tool_count: 1、basis: "inferred"。
tools_builder_search 则锁定 strategy: "deferred" 下 initial_tool_names 为 ["always", "lazy"](always_load 工具 + 其余初始位)。这些数字断言确保两侧 SDK 的派生公式(而非仅透传)也保持一致。
4.3 compress 家族:故障降级是契约的一部分
压缩相关的六个操作把 SDK 的"诚实降级"策略逐条钉死:
| 操作 | 触发条件 | 锁定的行为 |
|---|---|---|
compress |
正常 json 报告 |
逐字透传 Engine 报告(含 recovery_handle: "ccr_abc"、method: "elision"、lossless_to_model: false) |
compress_toon |
正常 toon 报告 |
透传 method: "toon"、lossless_to_model: true |
compress_passthrough |
"transport": "error" |
字节安全直通:output = 原始输入、ratio: 0.0、recovery_handle: null、token_count_basis: "unavailable" |
compress_bad_report |
报告含 tokens_before: "not-a-number"、ratio: "bad" |
视为坏报告,同样降级为直通 + 全零 + null |
compress_optimistic_ratio |
报告自称 ratio: 0.9 但 tokens_after: 80 / tokens_before: 100 |
SDK 重算 ratio 为 0.2,并把 basis 从 "verified" 降回 "inferred"——不允许 SDK 采信虚高的压缩率 |
compress_unchanged_false_claim |
报告声称压缩了但 output 与输入完全相同 |
判定为"虚报",全零 + null 降级 |
最后两个操作体现了该项目对 token 统计的一贯立场(与两侧 SDK 文档中"SDK 永不重造压缩器、永不臆造数字"的约定一致):ratio 可以由 SDK 依据 before/after 自行核算,basis 与 token_count_basis 必须如实披露计数依据。
4.4 checkpoint_expand:URL 编码的一致性
source_ref 取 "tenant a/ref+1",预期 path 为:
/sdk/v1/checkpoints/tenant%20a%2Fref%2B1/expand
空格、/、+ 全部百分号编码且两侧一致。这正是 AGENTS.md 编辑规则第三条的落地:"Keep values that encode identically in both languages (e.g. avoid ! ( ) * in expand refs — JS encodeURIComponent and Python quote disagree on those)"。JS 的 encodeURIComponent 与 Python 的 quote 对 ! ( ) * 这类字符的默认处理不一致,因此 fixture 刻意避开它们、并固化了两者都一致的 %20 / %2F / %2B 行为。
4.5 无随机性:id 注入 + 固定
OTLP 相关操作把"SDK 本应自己铸造 id"这件事变成了确定性断言:otlp_export 的 span 通过 input.span.trace_id / span_id 注入并原样出现在 body 的 traceId / spanId 中;otlp_export_traced 进一步验证 trace 内 exporter 复用 trace 的 id——traceId 是 trace 级的 aaaabbbb...,而 span 的 parentSpanId 是注入的 root_span_id。checkpoint、event_tool_call、artifacts_page 等操作同样通过 input.trace_id / input.span_id 注入 id,并让它们以固定值出现在 std_headers_traced 中。AGENTS.md 原文:"Nothing random may reach an assertion."
4.6 retry_loop_breaker 与 jobs_unavailable:本地行为也进契约
retry_loop_breaker:阈值 2 下连续三次相同调用,断言fired_at_call: 2、repeats: 3、threshold: 2;retry_loop_breaker_key_order用{"a":1,"b":2}与{"b":2,"a":1}交错,证明"相同参数"的判定与 JSON 键序无关;jobs_unavailable:cave.jobs.submit(...)必须本地抛出错误码cave_async_jobs_unavailable,且无 wire 段——即该占位 API 在任何版本里都不得发起网络请求(两侧 SDK 文档均注明:持久加密存储、凭据托管与排空 worker 落地之前,JobsClient只会在本地失败)。
五、编辑规则与运行方式(Release Gate 的日常)
修改这份契约必须遵循 AGENTS.md 的 "Editing rules":
- 新增字段/方法:给某一侧 SDK 加了字段或方法 → 在这里新增对应操作(或 body key)→ 另一侧的 harness 随即变红,直到它对齐。"That red is the point." 这是工作流的核心:红不是事故,是契约在起作用的证据;
- Header 键小写比对:因为 Python 的
urllib会把 header 键大写化,两侧 harness 都在捕获时把键小写后再比较(lowerKeys/k.lower()),要求跨语言匹配的是键集合 + 值,大小写本身不参与契约; - 编码一致性:fixture 中的取值必须保证在两种语言里编码后相同,避开
! ( ) *等两侧默认百分号编码规则不一致的字符(见 4.4 节的checkpoint_expand实例); - 零随机:SDK 会自铸的 id(trace id、span id)一律经操作
input注入,并固定写进预期 header 集合——otlp_export固定 span id 也是同一手法; - 运行门禁:
make product-test PRODUCT=sdk-ts && make product-test PRODUCT=sdk-python
该命令模式与仓库内其他产品(engine、proxy、mcp 等目录的 AGENTS.md / CLAUDE.md)的 make product-test PRODUCT=<name> 约定一致;TS 半边也可以 node --test tests/parity.runtime.mjs 直接驱动(需先构建 dist/),Python 半边直接 pytest。
两侧 SDK 的开发者文档也都把 parity 列为硬性约定:packages/sdk/typescript/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";packages/sdk/python/CLAUDE.md 则给出对称表述。本文开头引用的 See ../typescript/CLAUDE.md · ../python/CLAUDE.md 两个相对链接,在仓库中的全局路径即上述两个文件。
六、小结
packages/sdk/parity/ 用极小的面(一份 fixtures 文件 + 两份镜像 harness)解决了双语言 SDK 最容易腐化的问题:线协议漂移。它的要点可以浓缩为四条设计决策——
- 契约即数据:method、path、header 集合、body(或 body 键集合)、派生结果全部是
fixtures.json里的 JSON,语言中立; - 执行真实客户端:两侧 handler 调用的是 SDK 公共 API 而非协议复刻,mock 只替换传输层;
- 失败即信号:缺 handler、少操作、字段漂移都会让某一侧 CI 变红,且 harness 自带"fixture 过短/覆盖不全"的守卫断言;
- 确定性优先:id 注入、编码一致性、
transport: "error"触发降级分支,保证同一份契约在任何机器、任何语言运行时产生可复现的断言结果。
对维护者而言,日常动作就是:改一侧 SDK → 更新 fixtures.json → 运行 make product-test PRODUCT=sdk-ts && make product-test PRODUCT=sdk-python → 把另一侧改到绿为止。
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 StartedRust0624
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