OpenProgram Docs

Request building: Context → per-provider parameters#

The providers layer has a single responsibility: take a ready-made Context, translate it into the wire request for the current provider, and apply prompt caching according to that provider's mechanism.

providers does not care how the Context was built — how identity/tools/memory are assembled in the system prompt, whether to segment it, that is the concern of the upstream ../context/. This layer only handles "what to receive, what to translate, what to send".


1. Core: one unified format + one translation per provider#

Upstream produces a provider-agnostic Context; this layer dispatches by model.api to the corresponding provider's translation, converting it into the request that provider expects. All provider differences are contained in the translation layer, and upstream only ever deals with Context.

Context (already built upstream)
    │  dispatch by model.api
    ▼
that provider's translation: Context → its wire request

The industry-consensus approach (opencode / hermes / openclaw all do this).

2. Unified format: Context#

Context {
  system_prompt    system prompt (this layer treats it as ready-made content, not asking how it came to be)
  messages         conversation (user / assistant / tool_result)
  tools            tool list
}

The three are stored separately, with system standalone. This framework's Context uses standalone system as its baseline — translating to APIs with standalone system (Anthropic / Bedrock / Gemini) maps directly; when translating to the OpenAI family, that provider's translation folds system into messages.

3. Translation layer: where system lands + field mapping#

One translation per provider (_build_system / _build_messages / _build_tools), dispatched by model.api. The core differences are where system lands and the field names:

provider style where system lands conversation field tools field
anthropic-messages standalone system messages tools (strict inside the tool object)
openai-completions system/developer message goes into messages[0] messages tools (strict is a boolean)
openai-responses / codex extracted into the instructions parameter input tools
google (gemini) systemInstruction; assistant→model contents (content is called parts) tools
bedrock standalone system messages tools

The translation also smooths over each provider's idiosyncratic blocks: thinking-block signatures, whether tool_use arguments are objects or strings, and tool schema dialects (strict / additionalProperties).

4. Caching: three modes + a declaration layer#

Each provider's caching mechanism is fundamentally different, so don't force one abstraction over them all — split into three modes, with each provider declaring which one it belongs to.

mode who approach
explicit Anthropic, Bedrock place cache breakpoints explicitly in the request (cache_control / cachePoint)
auto OpenAI family no breakpoints, automatic prefix caching; can pass a cache key (prompt_cache_key)
none compatible providers without caching do nothing
out_of_band Gemini first call a separate API to store the cache object and get an ID, then carry the ID next time (two steps; for now only read hit stats)

The cache_spec declaration layer#

Copying the declarative paradigm of thinking under models/: one cache.json per provider declaring mode + the cache-key parameter name + TTL mapping + breakpoint limit. The shared module cache_spec.py loads it (get_cache_spec / cache_mode / ttl_for_retention / cache_key_param), and provider code reads the declaration to decide behavior, rather than hardcoding the rules in stream_simple. A provider with no declaration falls back to none (same as thinking's OpenAI-compatible fallback).

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

Where breakpoints go (explicit mode)#

The caller can explicitly mark cache_control on a content block, and it passes through verbatim to apply after that block (see ../../plans/cache-control-passthrough.md); when unmarked, the provider automatically marks the last block. The breakpoint limit (4 for Anthropic) is constrained by cache_spec's max_breakpoints; on overflow, the lower-priority ones are dropped by the tools > system > messages priority.

If breakpoint positions need to be decided uniformly by "the stable segments marked upstream", this relies on the existing per-block marker field TextContent.cache_control — upstream marks the block on a stable segment, and this layer passes it through. No new Context-level structure is introduced, nor is any change to the system_prompt type required.

5. The interface with upstream#

../context/                     providers (this layer)
build Context, decide content,  ──→   translate into each provider's wire, apply caching by mode
mark cache_control on blocks   Context     read cache_spec, pass through cache markers

The contract = Context (a content block may carry cache_control). How upstream builds the context is fully decoupled from this layer: adding a provider only touches this layer + one cache.json, and changing context building only touches ../context/.

6. The caching policy layer#

The caching policy layer follows opencode's cache-policy.ts + protocols/utils/cache.ts (references/opencode/packages/llm/src/), reproduced in Python:

item file the corresponding opencode mechanism
declaration loading providers/cache_spec.py + each provider's cache.json the RESPECTS_INLINE_HINTS "declare caching capability per provider" approach
automatic breakpoint policy apply_cache_policy in providers/cache_policy.py applyCachePolicy: mark the last tool + the most recent user message, without overriding the caller's manual markers
breakpoint budget _take/max_breakpoints inside cache_policy.py Breakpoints{remaining,dropped} + the 4-breakpoint limit
TTL bucketing _ttl_bucket in cache_policy.py ttlBucket (≥3600s → "1h", otherwise default 5m)
tool-level breakpoints the Tool.cache_control field + anthropic _build_tools passthrough opencode's ability to mark cache on tools too

Integration point: anthropic stream_simple calls apply_cache_policy before building messages/tools; _get_cache_control reads the ttl mapping and long_ttl_endpoints from cache.json rather than hardcoding them.

bedrock is also declared as explicit, but it uses cachePoint (a standalone block) rather than cache_control attached to a block, and it carries its own "place a breakpoint on the last message" logic, so it stays outside the unified apply_cache_policy; folding its tool breakpoints in is a possible later increment.

One difference from opencode: opencode's system is a segmented array and can mark a breakpoint individually on the "last system segment"; this layer's Context.system_prompt is a single string, and the system breakpoint is placed by each provider's _build_system on that whole block. The policy layer only covers tools + messages.

Last updated · 2026-08-13