Caveman Python SDK 详解:零依赖客户端的 API 全景、网关契约与跨语言一致性设计
本文围绕仓库中 packages/sdk/python 的开发者文档(AGENTS.md)展开,系统讲解 Caveman 项目的 Python SDK:一个仅依赖标准库(urllib.request、零第三方依赖)的 caveman_cloud 包,以及它如何把 Cave 入口、追踪连续性、延迟工具搜索、可恢复压缩、上下文打包、检查点/制品、重试熔断器与 OTLP 导出等能力映射到 Caveman Cloud 网关的 /sdk/v1/* 契约上。读完后你可以掌握该 SDK 的完整 API 面、请求头语义、跨语言一致性(parity)机制,以及编写/扩展该包时必须遵守的工程约定与红线。
包定位与基本事实
该包是一个仅用标准库实现的 Python 客户端,不提供任何压缩算法的本地实现,而是把所有重活委托给网关侧的 Engine。从 core.py 的导入列表可以确认:仅使用了 urllib.request、json、hashlib、secrets、threading 等标准库模块。
它对外提供三类核心对象(由 init.py 统一再导出):
Cave— 配置 + 入口点;CaveTool— 工具描述符;ToolSearchResult— 工具搜索返回结果。
所有 HTTP 调用都是向网关 POST,并携带从 Cave 字段派生的 x-cave-agent / x-cave-workflow / x-cave-retention 请求头。
发行名与导入名刻意分离
pyproject.toml 中有一处刻意设计值得注意:
# Public PyPI distribution name. `caveman` belongs to an unrelated project;
# keep distribution and import names distinct (`import caveman_cloud`).
name = "caveman-sdk"
version = "1.0.0"
requires-python = ">=3.13"
dependencies = []
- **发行名(PyPI)**是
caveman-sdk,因为caveman这个名字已被另一个不相关项目占用; - 导入包名保持
caveman_cloud(import caveman_cloud),两个名字不可混用; requires-python = ">=3.13",运行环境硬性要求 Python 3.13+;dependencies = []是红线之一,见后文「陷阱与红线」;- 附带
py.typed(见[tool.setuptools.package-data]),标记该包为 Typed 发行物; - 构建系统锁定
setuptools==80.9.0。
README.md 给出的最小可运行示例:
python -m pip install caveman-sdk
import os
from caveman_cloud import Cave
cave = Cave(
api_key=os.environ["CAVE_API_KEY"],
base_url="http://127.0.0.1:8787",
agent="support-agent",
)
result = cave.compress("large payload")
print(result.output, result.basis) # basis is inferred
在仓库内做可编辑安装则使用 python -m pip install -e .(在该包目录下执行)。
目录布局
按文档 AGENTS.md 的 Layout 一节,包结构为:
| 路径 | 职责 |
|---|---|
| caveman_cloud/init.py | 再导出 Cave、CaveTool、CompressResult、ContextPackItem、ContextPackOptions、ContextPackResult、ToolSearchResult 等 |
| caveman_cloud/core.py | 全部实现:Cave、Trace、Provider、_Create、ToolSearchResult、CompressResult、ContextPack*、CaveTool、headers() |
| tests/test_sdk.py | pytest 测试;用 patch() 模拟 urllib.request.urlopen |
| tests/test_parity.py | 跨语言一致性测试套件;驱动 packages/sdk/parity/fixtures.json(与 sdk-ts 共享同一份 fixtures)。同一字段只存在于一个 SDK 而不在另一个里,CI 直接失败 |
| tests/test_trace_continuity.py | trace/span id 铸造 + 哪些请求携带 x-cave-trace-id / x-cave-parent-span-id;镜像 TS 端的 tests/trace-continuity.runtime.mjs |
从 tests/ 目录实际内容看,测试面比文档列举的三件更广,还包括 test_assembly.py、test_context_pack.py、test_exporter.py、test_runtime_policy.py、test_shared_context.py、test_task_profile.py、test_tool_events.py、test_packaging.py、test_structural.py 等,与下文各 API 面一一对应。
核心 API 面
core.py(约 2600 行)是全部实现所在。以下按文档的 Key API surface 逐条展开,并补充源码级佐证。
1. Cave.trace():上下文管理器与追踪连续性
Cave.trace(workflow, tags, *, trace_id=None, span_id=None) 是一个上下文管理器,yield 出 Trace 对象;在 with 块内调用 .model["openai"].responses.create(body) 等 provider 调用。
关键行为(源码见 Cave.trace 的 docstring):
- trace 用与 OTel exporter 同一个 RNG 铸造
trace_id(32 位小写 hex)和根span_id(16 位小写 hex); - 传入
trace_id/span_id表示延续入站 trace;如果值不符合精确的 hex 形状(_normalize_trace_id/_normalize_span_id会校验),会被替换为一个新铸造的值,而不是原样发到线上; - 通过 trace 发出的每个 provider 调用都携带
x-cave-trace-id+x-cave-parent-span-id; - 通用
/sdk/v1/*调用与直接从Cave构建的 provider 不带这两个头——唯一的 SDK 端点例外是Trace.tool,它的/sdk/v1/events调用会携带 trace id 和根 parent span id。
这一"哪些请求携带 trace 头"的规则正是 tests/test_trace_continuity.py 固化的契约。
2. Trace.exporter() 与 Cave.exporter():OTel 集成
Trace.exporter(service_name=None)返回一个OTelExporter,其default_trace_id就是该 trace 的 id——这样 SDK 自己的 span 与网关侧的请求行(request rows)能汇入同一条 trace。它镜像 TS 端的CaveTrace.exporter;Cave.exporter(service_name=None)则返回独立于具体 trace 的OTelExporter:record_span(...)把当前 GenAI 字段映射到gen_ai.*语义约定;export()以 OTLP/JSON 格式 POST 到标准/v1/traces路径(请求头经otlp_headers()构造;旧路径/otlp/v1/traces保留为仅服务端兼容)。
从源码结构看,该 exporter 甚至自带了一个纯 Python 的 Ed25519 校验实现(_ed25519_verify,core.py L1814-L1930),用于运行时策略响应的签名验证——这是"零第三方依赖"约束下的直接后果:不能引 cryptography,就只能手写椭圆曲线校验。
3. Cave.tools():延迟加载(deferred)工具目录
Cave.tools(catalog, *, strategy="all", initial_tool_count=8) 返回一个 builder 句柄(_ToolsHandle),提供:
.strategy、.initial(list[CaveTool]);.search(query, *, max_tools, context, workflow, ranker, session_id)。
语义要点:
strategy="deferred"时,把每一个always_load=True的工具恰好包含一次,剩余 initial 名额由非强制工具补足;如果initial_tool_count小于强制工具数量,会在本地直接失败(不发请求);.search()总是携带完整目录打向网关的/sdk/v1/tool-search端点——SDK 端只做编排,检索发生在服务端。
CaveTool 本身是一个简单 dataclass(core.py L250-L258):name、description、input_schema,以及可选的 read_only、idempotent、always_load 布尔标注。
4. Cave.tool_search():扁平变体
Cave.tool_search(tools, query, *, context, max_tools, workflow, ranker, session_id) 是 tools().search() 的扁平写法:把 [tools, query] POST 到 /sdk/v1/tool-search,返回 ToolSearchResult:
.saved_tokens/.reduction_pct/.session_id;- schema 的 token 计数器是估算——
.token_basis字段披露所用计数器,.basis恒为"inferred"; ranker("bm25"或"embeddings")原样透传——SDK 从不自己计算相似度,排序完全由网关完成。
从 ToolSearchResult 的实现 可以看到一个跨语言细节:reduction_pct 使用 math.floor(value * 10.0 + 0.5) / 10.0 而非 Python 内建 round,因为 Python 的银行家舍入与 Go 的 math.Round / JS 的 Math.round 在平局点上不一致——这是跨语言 parity 契约在数值层面的具体体现。
5. Cave.prompts.internal_brevity():输出风格片段
Cave.prompts.internal_brevity(*, style, preserve_errors_verbatim=False, preserve_code_verbatim=False) 生成一段输出风格提示片段;style="none" 时返回空字符串。两个布尔参数渲染为小写,以匹配 TS 端的 cave.prompts.internalBrevity——即 Python 端在 wire 层面刻意保持与 TS 完全一致的文本形态。
6. Cave.compress():字节安全的委托压缩
Cave.compress(payload, *, content_type=None) → CompressResult;POST /sdk/v1/compress 并映射 Engine 的报告。核心契约是字节安全直通(byte-safe pass-through):任何传输/解析问题发生时,返回原始输入、ratio=0.0、无恢复句柄(fail-closed)。.token_count_basis 披露计数器,basis 恒为 "inferred"("verified" 只有 Cloud 的 active 路径才有资格发出)。SDK 只做委托,绝不重新实现压缩器。
CompressResult 的字段包括 output、content_type、tokens_before/after、ratio、basis、recovery_handle、method、lossless_to_model、token_count_basis。
7. Cave.context.pack():连接态、有意的有损选择
Cave.context.pack(query, items, options) → ContextPackResult;仅在已连接(connected-only)时 POST 到 /sdk/v1/context/pack。要点:
- 这是对调用方自有 item 的有损选择器,与 CCR/ledger 无关;
- 返回精确的
deferred_ids(被推迟/未入选的 item id 列表); - 传输失败或报告畸形时,返回全部原始 item 且推断节省量(inferred savings)为零;
ContextPackOptions提供max_tokens、reserve_tokens、recency_half_life_ms、recency_weight、error_boost、now等预算与打分参数;- 设计分工:context packing 只决定"什么进入窗口",缓存最优装配(assembly)决定"放在哪个位置"。
8. Trace.checkpoint() / Trace.expand():可逆检查点
Trace.checkpoint(messages, options)— POST/sdk/v1/checkpoints;网关将其持久化(Valkey)并返回一个可逆的source_ref;Trace.expand(source_ref)— 是checkpoint()的 GET 半边:GET /sdk/v1/checkpoints/{ref}/expand返回存储的{source_ref, version, messages, checkpoint}。
9. Provider 代理与 Bedrock 路由描述符
Cave.openai / anthropic / gemini / vertex(upstream_key)→ 返回Provider,请求经网关代理转发;Provider.raw(path, body)是逃生舱(镜像 TS provider-client 的raw);- 在 trace 内:
Trace.model["openai"].responses.create(body, *, latency_class=None, tool_session_id=None):- 设置了
latency_class时发送x-cave-async头("interactive"时为"false",否则"true"); - 设置了
tool_session_id时发送x-cave-tool-session; - 镜像 TS 的
trace.model.openai.responses.create(body, {cave:{latencyClass, toolSessionId}});
- 设置了
Cave.bedrock(region, endpoint="runtime")→ 返回一个不发网络请求的第一方路由描述符:Runtime 默认指向/bedrock,显式 Mantle 返回/bedrock/anthropic;sdk_only=False镜像 TS 的sdkOnly。
10. Trace.tool() 与制品(artifacts)
Trace.tool(name, options, fn)— 先调用fn(),随后 POST 一条tool.call事件(/sdk/v1/events);Trace.page_artifact(value, options)/Trace.artifacts.page(value, options)— 发送版本化的{value, options, workflow},网关只存储 JSON 形态的value;artifacts.get(id)执行带认证的取回(authenticated GET)。page_artifact是保留向后兼容的别名。
11. Cave.retry_loop_breaker(threshold=3):重试熔断器
返回 RetryLoopBreaker:
.record(name, args)— 在连续threshold次相同工具调用(同名 + 同参数签名)后抛出RetryLoopError,从而打断卡死的循环;.guard(name, args, fn)— 记录(可能抛错)后再执行fn。
源码实现 说明了签名如何生成:signature() 用 json.dumps(arguments, sort_keys=True, separators=(",", ":")) 生成规范 JSON,拼成 name(args);每次相同签名使 _repeats 递增,不同签名则重置计数;触发点是"第 threshold+1 次"连续相同调用(self._repeats > self.threshold 时抛错)。文档同时指出这与 TS 端 RetryLoopBreaker 的字段名与阈值语义完全一致。
12. Cave.jobs:预留的异步作业面
Cave.jobs 是一个预留的 JobsClient 面:它的每个方法都在本地直接失败(cave_async_jobs_unavailable),在"持久化加密请求存储、凭据托管、draining worker"三者就绪之前不发任何网络请求。它镜像 TS 端的 Cave.jobs——这是两个 SDK 共同的前向兼容占位,而非可用功能。
请求头契约:headers() 是单一事实来源
所有出站请求头由唯一函数 headers() 生成,实际拼装如下:
| 请求头 | 来源/条件 |
|---|---|
content-type: application/json |
恒定 |
authorization: Bearer {api_key} |
Cave.api_key |
x-cave-agent |
Cave.agent |
x-cave-workflow |
传入的 workflow 参数 |
x-cave-retention |
Cave.retention(默认 "metadata") |
x-cave-user-hash |
设置 Cave.user 时(不透明终端用户标识,非 Caveman 成员 id) |
x-cave-upstream-key |
提供上游 key 时 |
x-cave-async |
设置 latency_class 时;"interactive" 为 "false",否则 "true" |
x-cave-tool-session |
设置 tool_session_id 时 |
x-cave-assembly |
装配(assembly)元数据头 |
x-cave-trace-id / x-cave-parent-span-id |
仅通过 Trace 发出的请求携带 |
一个实现细节值得注意:x-cave-async 的注释明确写道"gateway 可以自由忽略该头;两个 SDK 必须发送完全相同的 wire"——即该头是纯调度提示,SDK 端只保证 wire 一致性。另外,Cave 的 default_workflow 支持 CAVE_WORKFLOW 环境变量(规范为小写 [a-z0-9_-]、最长 96),非法的环境值会被忽略(回落到 unlabeled-workflow)而不是让每个请求都 400。
从源码中还能看到 SDK 覆盖的全部网关端点:/sdk/v1/tool-search、/sdk/v1/compress、/sdk/v1/cave-plan、/sdk/v1/runtime-policy、/sdk/v1/shared-context[/{key}]、/sdk/v1/context/pack、/sdk/v1/events、/sdk/v1/artifacts、/sdk/v1/checkpoints,以及 OTel 的标准 /v1/traces。
工程约定(Conventions)
文档为后续维护者定下四条硬性约定:
- 测试永不触网:所有测试使用
patch("urllib.request.urlopen", side_effect=fake_urlopen)模拟网络,禁止真实网络请求; - 新端点的唯一入口:新增网关端点一律通过
Trace._request(path, body)或Provider.create(path, body)添加; - 请求头单点维护:
headers()是所有出站请求头的唯一来源——只改它,别处一律不改; - 延迟工具搜索的会话交接使用请求/结果的
session_id加 provider 头x-cave-tool-session;任何相关改动必须同时更新 sdk-ts 和 parity fixtures。
运行测试只需在该包目录下执行 pytest(要求 Python ≥ 3.13)。
陷阱与红线(Gotchas)
这四条是维护该包时最不能碰的红线:
- 零第三方依赖——不要引入
requests、httpx或任何库;保持 pyproject.toml 中dependencies = []。这是包的核心定位,OTel 校验手写 Ed25519 也是这一约束的代价体现; - 字节安全(byte-safe)——SDK 原样把请求体发给网关,不做任何改写;
compress()委托 Engine,且在任何问题时直通返回原始输入; - 上下文打包是连接态且有意有损——它把 item 字节发往网关,从不在本地 wrap 执行,且依赖调用方自行保留所有被
deferred_ids点名的 item。选择进入窗口内容由它负责,缓存最优装配负责放置; - Python 与 TS 双 SDK 镜像同一组字段名和
/sdk/v1/*契约——这一约束由共享 parity 套件(tests/test_parity.py + packages/sdk/parity/fixtures.json)强制执行,而不只是惯例。任何分歧都是 CI 失败。改一个 SDK,就必须同时改另一个和 fixtures。
小结
packages/sdk/python 是 Caveman 多语言 SDK 家族中的 Python 一员:以 Cave 为入口、以 headers() 为 wire 单点、以"SDK 只编排、网关做计算"为分工边界,用纯标准库覆盖了追踪连续性、延迟工具搜索、委托压缩、有损上下文打包、可逆检查点、制品存取、重试熔断与 OTLP 导出。其设计约束——零依赖、字节安全、fail-closed、与 TS 端由共享 fixtures 强制对齐——共同保证了 Python 用户获得与 TypeScript 用户完全一致的网关行为。完整的包级背景可进一步参阅仓库根的 CLAUDE.md。
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 StartedRust0627
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