OpenProgram Docs

请求构建:Context → 各 provider 参数#

providers 层的唯一职责:拿到一个准备好的 Context,翻译成当前 provider 的 wire 请求,并按该 provider 的机制落地 prompt 缓存。

providers 不关心 Context 是怎么构建出来的 —— 系统提示里身份/工具/记忆怎么拼、 要不要分段,那是上游 ../context/ 的事。本层只"收什么、翻译什么、 发什么"。


一、核心:一份统一格式 + 每 provider 一个翻译#

上游产出一个 provider 无关的 Context;本层按 model.api 分发到对应 provider 的 翻译,转成那家认的请求。provider 差异全收在翻译层,上游永远只跟 Context 打交道。

Context(上游已构建好)
    │  按 model.api 分发
    ▼
该 provider 的翻译:Context → 它的 wire 请求

业界共识做法(opencode / hermes / openclaw 均如此)。

二、统一格式:Context#

Context {
  system_prompt    系统提示(本层视作已准备好的内容,不问怎么来的)
  messages         对话(user / assistant / tool_result)
  tools            工具清单
}

三者分开存,system 独立。本框架的 Context 以 system 独立为基准 —— 翻译给 system 独立的 API(Anthropic / Bedrock / Gemini)直接对应;翻译给 OpenAI 一类时 由该 provider 的翻译把 system 落进 messages。

三、翻译层:system 落点 + 字段映射#

每 provider 一段翻译(_build_system / _build_messages / _build_tools),按 model.api 分发。核心差异在 system 的落点和字段名:

provider 风格 system 落到哪 对话字段 工具字段
anthropic-messages 独立 system messages tools(strict 在 tool 对象内)
openai-completions system/developer 消息进 messages[0] messages tools(strict 是 boolean)
openai-responses / codex 抽成 instructions 参数 input tools
google (gemini) systemInstruction;assistant→model contents(内容叫 parts) tools
bedrock 独立 system messages tools

翻译还抹平各家特有的块:思考块签名、tool_use 参数是对象还是字符串、工具 schema 方言(strict / additionalProperties)。

四、缓存:三种 mode + 声明层#

各家缓存机制根本不同,不用一个抽象硬覆盖,分三种 mode,每 provider 声明自己属哪种。

mode 做法
explicit Anthropic, Bedrock 请求里显式打缓存断点(cache_control / cachePoint)
auto OpenAI 系 不打断点,自动前缀缓存;可传缓存键(prompt_cache_key)
none 无缓存的兼容 provider 什么都不做
out_of_band Gemini 先调独立 API 存缓存对象拿 ID,下次带 ID(两步;暂仅读命中统计)

cache_spec 声明层#

照搬 models/ 下 thinking 的声明式范式:每 provider 一份 cache.json 声明 mode + 缓存键参数名 + TTL 映射 + 断点上限。公共模块 cache_spec.py 加载它 (get_cache_spec / cache_mode / ttl_for_retention / cache_key_param), provider 代码读声明决定行为,而不是把规则硬编码在 stream_simple 里。无声明的 provider 走 none 兜底(和 thinking 的 OpenAI 兼容兜底同理)。

{
  "mode": "explicit",
  "breakpoint_format": "cache_control",        // 或 "cachePoint" (bedrock)
  "retention_ttl_map": {"short": null, "long": "1h"},
  "max_breakpoints": 4
}
{ "mode": "auto", "cache_key_param": "prompt_cache_key" }
{ "mode": "none" }

断点打在哪(explicit 模式)#

调用方可在某个 content block 上显式标 cache_control,原样透传到该 block 之后 (见 ../../plans/cache-control-passthrough.md); 未标时 provider 自动在最后一块打。断点上限(Anthropic 4 个)由 cache_spec 的 max_breakpoints 约束,超了按 tools > system > messages 优先级丢低的。

缓存断点位置如需按"上游标的稳定段"统一决定,靠的是 TextContent.cache_control 这个已有的逐块标记字段 —— 上游在稳定段的 block 上标,本层透传。不引入新的 Context 级结构,也不要求改 system_prompt 类型。

五、与上游的接口#

../context/                     providers(本层)
构建 Context、决定内容、    ──→   翻译成各家 wire、缓存按 mode 落地
在 block 上标 cache_control   Context     读 cache_spec、透传缓存标记

契约 = Context(content block 可带 cache_control)。上游怎么构建上下文与本层 完全解耦:加 provider 只动本层 + 一份 cache.json,改上下文构建只动 ../context/

六、缓存策略层#

缓存策略层沿用 opencode 的 cache-policy.ts + protocols/utils/cache.ts (references/opencode/packages/llm/src/),用 Python 复刻:

文件 对应的 opencode 机制
声明加载 providers/cache_spec.py + 各 provider cache.json RESPECTS_INLINE_HINTS 那套"按 provider 声明缓存能力"
自动断点策略 providers/cache_policy.pyapply_cache_policy applyCachePolicy:标最后一个 tool + 最近 user 消息,不覆盖调用方手动标记
断点预算 cache_policy.py_take/max_breakpoints Breakpoints{remaining,dropped} + 4 断点上限
TTL 分桶 cache_policy.py_ttl_bucket ttlBucket(≥3600s → "1h",否则默认 5m)
tool 级断点 Tool.cache_control 字段 + anthropic _build_tools 透传 opencode 给 tool 也标 cache 的能力

接入点:anthropic stream_simple 在构建 messages/tools 前调 apply_cache_policy; _get_cache_controlcache.json 的 ttl 映射与 long_ttl_endpoints,而不是硬编码。

bedrock 也声明为 explicit,但它用 cachePoint(独立块)而非挂在 block 上的 cache_control,且自带"在最后一条消息打断点"的逻辑,因此不走统一的 apply_cache_policy;把它的 tool 断点纳进来是可能的后续增量。

与 opencode 的一处差异:opencode 的 system 是分段数组,能在"最后一段 system"上 单独标断点;本层 Context.system_prompt 是单字符串,system 断点由各 provider 的 _build_system 在那一整块上打。policy 层只覆盖 tools + messages。

Last updated · 2026-08-13