首页
/ Caveman Python SDK 详解:零依赖客户端的 API 全景、网关契约与跨语言一致性设计

Caveman Python SDK 详解:零依赖客户端的 API 全景、网关契约与跨语言一致性设计

2026-09-06 11:51:10作者:沈韬淼Beryl

本文围绕仓库中 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.requestjsonhashlibsecretsthreading 等标准库模块。

它对外提供三类核心对象(由 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_cloudimport 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 再导出 CaveCaveToolCompressResultContextPackItemContextPackOptionsContextPackResultToolSearchResult
caveman_cloud/core.py 全部实现:CaveTraceProvider_CreateToolSearchResultCompressResultContextPack*CaveToolheaders()
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.pytest_context_pack.pytest_exporter.pytest_runtime_policy.pytest_shared_context.pytest_task_profile.pytest_tool_events.pytest_packaging.pytest_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_verifycore.py L1814-L1930),用于运行时策略响应的签名验证——这是"零第三方依赖"约束下的直接后果:不能引 cryptography,就只能手写椭圆曲线校验。

3. Cave.tools():延迟加载(deferred)工具目录

Cave.tools(catalog, *, strategy="all", initial_tool_count=8) 返回一个 builder 句柄(_ToolsHandle),提供:

  • .strategy.initiallist[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):namedescriptioninput_schema,以及可选的 read_onlyidempotentalways_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 的字段包括 outputcontent_typetokens_before/afterratiobasisrecovery_handlemethodlossless_to_modeltoken_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_tokensreserve_tokensrecency_half_life_msrecency_weighterror_boostnow 等预算与打分参数;
  • 设计分工: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/anthropicsdk_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 形态的 valueartifacts.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 一致性。另外,Cavedefault_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)

文档为后续维护者定下四条硬性约定:

  1. 测试永不触网:所有测试使用 patch("urllib.request.urlopen", side_effect=fake_urlopen) 模拟网络,禁止真实网络请求;
  2. 新端点的唯一入口:新增网关端点一律通过 Trace._request(path, body)Provider.create(path, body) 添加;
  3. 请求头单点维护headers() 是所有出站请求头的唯一来源——只改它,别处一律不改;
  4. 延迟工具搜索的会话交接使用请求/结果的 session_id 加 provider 头 x-cave-tool-session;任何相关改动必须同时更新 sdk-ts 和 parity fixtures。

运行测试只需在该包目录下执行 pytest(要求 Python ≥ 3.13)。

陷阱与红线(Gotchas)

这四条是维护该包时最不能碰的红线:

  1. 零第三方依赖——不要引入 requestshttpx 或任何库;保持 pyproject.tomldependencies = []。这是包的核心定位,OTel 校验手写 Ed25519 也是这一约束的代价体现;
  2. 字节安全(byte-safe)——SDK 原样把请求体发给网关,不做任何改写;compress() 委托 Engine,且在任何问题时直通返回原始输入;
  3. 上下文打包是连接态且有意有损——它把 item 字节发往网关,从不在本地 wrap 执行,且依赖调用方自行保留所有被 deferred_ids 点名的 item。选择进入窗口内容由它负责,缓存最优装配负责放置;
  4. 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

登录后查看全文
热门项目推荐
相关项目推荐