Skip to content

A run resolves one model card, and every control is decided against it before the first call ​

The ask. Decide (the maintainer, before the program plan is amended and before any unit is built): adopt the model card as the data model behind switching harnesses and models across providers by config alone: the ref grammar, the provider block's declaration, the per-run resolution recorded on the run, the meter row with its price source, one proxy route per wire shape, and the permutation matrix as the conformance suite. Written for an engineer who knows the model proxy and the harness contract (record 0038) and has not read the handoff issue (#1532), whose frame this record inherits. Every API claim below cites the pinned library or the provider's documentation as read on 2026-09-17.

Success criteria the decision is judged against:

  1. An operator switches harness and model across providers by config alone; outside the card's own default tables, no code path names a provider, a vendor or a model.
  2. Every control we expose (effort, the output cap, tool choice, cache retention, image and document inputs, the compaction window, price) is native where the wire has it, degraded with a note on the run's record, or refused before the first call. Never a silent downgrade.
  3. The permutations are the conformance suite: harness × provider block × control, each cell native, degraded or refused; a wire, a block or a control without its row fails verify by name.
  4. Every metered turn carries its biller, vendor, model, price and price source, and each biller's invoice ties out against the rows.

TL;DR ​

Both harnesses run every model on a card we invent: a 200,000-token window, a zero price, no level map, and an effort tier that pi clamps or the provider ignores while the record says only what was asked. The bet is that the bot resolves one model card per run at dispatch, layering the operator's block over pi's pinned registry over the wire's defaults, writes it into whichever harness runs and onto the run's record, and decides each control before the first call as native, degraded by name, or refused. The cost is a dependency on pi's registry as the catalog, a per-block config surface, and refusals where a silent downgrade used to pass. Doing nothing leaves 1M models compacting at 200k, xhigh silently high, and every OpenRouter turn unpriced.

Today at df468d8f ​

You would expect our controls to stop at the compatible adapter. They do not. Five facts the design depends on; the survey with every proof is the appendix.

FactProof
A run's pi already sends reasoning_effort: <tier> on the chat route to any block, because we declare the run's card reasoning: true; with no level map pi clamps xhigh and max to high at launch, and nothing records it. The router's and reflection's cards say reasoning: false. OpenCode drops the tier on every provider.src/core/harness/pi/process.ts:237; pi-ai api/openai-completions.js:727-736, models.js:550-581; src/core/harness/piAi.ts:151; src/core/harness/opencode/harness.ts:117
Both harnesses are handed an invented card: contextWindow: 200_000, a zero rate card, no level map. Compaction fires at 200k minus the reserve for a 1M Claude and a 128k local model alike; pi computes a $0 price per turn and the proxy's meter records four counters and the ref.pi/process.ts:237-242; opencode/process.ts:68, 321-322; src/channels/modelProxy.ts:489-510
Nothing knows a vendor. The ref is split at its first slash in five call sites and three by-hand splits; openrouter/anthropic/claude-sonnet-4 is unpriced because the prefix-dropped id is not an Anthropic family. pi adds Anthropic cache markers through OpenRouter only for a provider literally named openrouter, and sends OpenAI's cache key on the chat wire only to a URL containing api.openai.com or under long retention; a run's provider is the proxy, so the example config's promise of cache markers on a run is false.src/core/provider.ts:108; src/core/modelPricing.ts:137-142, 164; pi-ai openai-completions.js:588-591, 1276; config/config.example.yaml:34-35
The APIs moved. OpenAI recommends Responses and its chat route loses tool calling with any effort but none from GPT-5.4. OpenRouter returns usage.cost on every response (the include flag is a deprecated no-op), splits a BYOK request's fee from its upstream cost, and takes our five effort words plus minimal and none. Anthropic rejects budget_tokens from 4.7 on and bills 1M context at standard rates on 4.6 and later.the research, read 2026-09-17
Effort is validated as a word at load and never against the model it reaches; run_meta.effort records the tier asked, never the tier applied. Documents never reach any provider on any harness: pi's user content is text and images, and every path writes a text stub without a typed note.src/config/validate.ts:118-141; src/core/runEvents.ts:576; pi/harness.ts:225-227, pi/mirror.ts:380-383, piAi.ts:337-343

The shape ​

A model card is the resolved description of one <block>/<model> for one run: the block's wire (anthropic-messages, openai-chat or openai-responses), the biller (the block), the vendor (the block's declared vendor, or for an aggregator the id's first segment), the levels the model takes (our five effort tiers each mapped to the wire's word or refused, or unknown as a whole), the cap field the wire spells the output cap with, the window, the inputs it accepts, its cache rule (automatic, markers, none, unknown) and its price source. Three layers, highest first: the operator's block (providers.<block>.models.<id> and the block's defaults), the pi registry file the block's catalog names, and the wire's structural defaults (vendor-keyed tables, the one place a vendor's name may appear in code). The dispatcher resolves the card once, where it already checks the block exists (resolveTarget, src/core/dispatch/resolve.ts), writes it into pi's models.json and OpenCode's configuration in place of the invented card, and records it as run_meta.card with each field's provenance. A control is then native (the wire's own word goes out, vouched for by a layer), degraded (the card cannot vouch, in one of two forms the note names: a fallback, where the applied word differs from the asked one, or an unvouched send, where the asked word goes out and the note says no layer stands behind it; either way one typed control_degraded note lands before the first call), or refused (the run never starts and the reply names the model, the control and what it takes, as provider_unconfigured does today). The proxy keeps pinning two body fields and gains one route per wire; the meter reads a provider-reported cost when the wire carries one and prices from the card otherwise, naming the source on the span.

The closest known shape is LiteLLM's model_list: one entry per model with its provider, its parameters and its price, consulted on every call. The one difference is that we resolve the entry once per run and write it onto the run's record and into the harness, so the record and both harnesses agree on what the model can do, instead of translating each call at a gateway.

One trace: a review on openrouter/deepseek/deepseek-v4.1-flash at effort: xhigh with a document attached ​

The case most likely to be wrong is a model the catalog does not know, on an aggregator. OpenRouter listed deepseek/deepseek-v4.1-flash on 2026-09-10: 1,048,576 context, 384,000 output, a base of $0.15 / $0.60 / $0.003 per million (input, output, cache read) with weekday time-of-day overrides that double it, text and image, supported_parameters naming reasoning, reasoning_effort, tools, tool_choice, structured_outputs and max_tokens, but neither max_completion_tokens nor parallel_tool_calls (its public models API, read 2026-09-17). pi's registry at 0.85.1 has no card for it.

  1. A person posts a pull request link with effort: xhigh in a channel whose review model is openrouter/deepseek/deepseek-v4.1-flash; the block reads wire: openai-chat, vendor: model, catalog: openrouter, baseUrl: https://openrouter.ai/api/v1, apiKeyEnv: OPENROUTER_API_KEY.
  2. The dispatcher resolves the card: block openrouter, model deepseek/deepseek-v4.1-flash, vendor deepseek. The catalog has no card and the operator names no override, so every field falls to the wire's defaults and the provenance says so.
  3. Levels unknown: degraded, unvouched. The dispatcher writes the identity map for all five tiers into models.json, so pi admits xhigh and clamps nothing, and notes control_degraded {control: effort, asked: xhigh, applied: xhigh, vouched: false}.
  4. Cap field unknown: degraded. The wire default for openai-chat is max_completion_tokens, which this model does not list; the note says the cap went out under a field the model may ignore.
  5. Inputs unknown: the document degrades to a text stub with the note, as on every card until a harness unit carries files; an image would go out unvouched.
  6. Cache: the vendor table says DeepSeek caches automatically through OpenRouter; native, no markers. Had the vendor been anthropic, the card would say markers and the dispatcher would write compat.cacheControlFormat: "anthropic", the switch pi keys on the provider name today.
  7. Window unknown: degraded to 128,000, pi's own default for a models.json card, with the note. The 1M models the TL;DR names are in the registry; this default reaches only a model no file lists, and compacting early beats overflowing a window it may not have.
  8. pi sends reasoning_effort: xhigh, max_completion_tokens, store: false; the proxy pins the model and the cap and forwards the rest byte-for-byte to /chat/completions.
  9. OpenRouter streams; its final chunk carries usage with the counters, cost and is_byok: false. The meter records the four counters, usd as the reported cost, priceSource: provider, biller: openrouter, vendor: deepseek. Had is_byok been true, usd would be cost plus cost_details.upstream_inference_cost, since cost is then OpenRouter's fee alone.
  10. The run page shows four notes before the first turn and a price on every turn; the daily tie-out compares the summed rows with GET /api/v1/activity for the openrouter biller.
  11. The operator runs providers check, which reads OpenRouter's endpoints for the model and reports the window, the image support, the reasoning parameters and the cap field against the card's unknowns, and pins models.deepseek/deepseek-v4.1-flash: {window: 1048576, inputs: {image: true}, levels: {high: high, xhigh: xhigh}, capField: max_tokens}. The next run has no notes.
  12. Variant, openrouter/deepseek/deepseek-v4-pro at effort: max: the registry's OpenRouter file maps high and xhigh and marks max null, while its DeepSeek file maps high and max for the same model direct. Through OpenRouter max is refused before the call; direct it is native. The card is per block and model, because the biller decides what reaches the vendor.

The property the trace proves: an unknown card degrades exactly the controls it cannot vouch for, says so on the record before the first token, prices the turn with the biller's own figure, and gives the operator one command and one override to make the next run native; a known card refuses before anything is spent.

The difficulty map ​

Ranked by the risk of being wrong.

  1. Levels (Levels). The truth is per block and model, three layers each downgrade silently today, and the unknown default decides whether a local server can run at all.
  2. Price (Price). Three sources of different finality; OpenRouter's figure is credits and splits on BYOK; mixing sources in one sum is the failure.
  3. The grammar and the block (Grammar). Vendor and catalog must be declared, not inferred, and type must keep loading.
  4. Wires and gateways (Wires). The Responses route is required for OpenAI direct, and a staged LLM gateway must be placed on one side of the proxy or the other.
  5. The matrix (Matrix) (most work). Every cell replays through the harness seam against a capturing upstream; the lint names the missing row.

Levels: the tier is decided against the card, never clamped at launch ​

The constraint: our five tiers reach three translators that each change them without a trace. pi clamps a tier the card does not admit to the nearest admitted one, upward first, at session start (models.js:550-581); OpenRouter maps effort onto Anthropic as a budget ratio and onto Gemini as a level, and documents no error for a model without reasoning; OpenAI documents per-model subsets (GPT-5 takes minimal to high; GPT-5.5 none to xhigh; GPT-6 Astra returns 400 for none). Anthropic takes the five words as output_config.effort on the models that list them (xhigh only on the newest generations) and rejects the budget path from 4.7 on. The per-model catalogs are pi's registry, whose files are keyed wire, then id, then card, and whose thinkingLevelMap marks a level null when the model refuses it and admits xhigh and max only when named; and OpenRouter's supported_parameters per endpoint, which says a model takes reasoning and not which values.

The design: the card's levels maps each of our tiers to the wire's word or refused, or is unknown as a whole. The operator's models.<id>.levels wins; then the registry card, read by pi's own rule (null is refused; low, medium, high are native unless null; xhigh and max are native only when named, else degraded down to the highest named tier below them); then the wire default, unknown on every wire. The dispatcher writes the resolved map into models.json, the identity map when levels are unknown, so pi admits what the card says and clamps nothing; on OpenCode it writes one model variant per tier, each an id plus the request body overlay that spells the word (packages/schema/src/config/provider.ts:55-58 at 2.0.3), and the session selects the tier as <model>#<tier>. A tier mapped refused refuses before the call; reasoning: false with a tier asked by a directive or a scope refuses, with none asked is native without thinking; no tier asked on a known card is native, the model's own default. The card is per block and model: pi's registry maps deepseek-v4-pro to high and xhigh in its OpenRouter file and to high and max in its DeepSeek file.

The cap field, the window and the inputs follow the same layers. The cap field is native when the operator's override or the registry card's compat names it; otherwise the wire default goes out unvouched (max_completion_tokens on openai-chat, max_output_tokens on Responses, max_tokens on Anthropic), and providers check is what reads the model's parameter list to pin it. An unknown window degrades to 128,000; unknown image support sends the image unvouched; a document degrades on every card. Cache retention is a vendor-keyed table in the wire defaults: markers for Anthropic on any wire, automatic for OpenAI, DeepSeek, Groq, Moonshot, Grok and Gemini through OpenRouter. The dispatcher writes the markers rule as pi's compat.cacheControlFormat, the switch pi keys on a provider name today. OpenAI's cache key has no such switch on the chat wire, where pi sends prompt_cache_key only to its own host or under long retention (its Responses adapter sends it on any URL), so an openai-chat block on OpenAI direct through the proxy has a degraded cache-key cell until the harness write unit sets long retention there.

Invariants a test can check: the word on the wire is the word the card mapped, never one pi chose; a control_degraded note exists for a control if and only if it is not native, and its vouched field says which form; a refusal spends no turn and opens no span; run_meta.effort keeps the asked tier and run_meta.card says what was applied. Failure mode: a registry card that lists a level the provider now rejects surfaces as the provider's 400 on the first turn, ending the run failed with the provider's words, the same failure as today with a better record; providers check is how an operator finds it first. The alternative it beat: letting pi's clamp stand and reading the level back. pi's launch path reports no clamp, OpenCode has no clamp to report, and a design that reads the truth after the call cannot refuse before it.

Price: every turn names its source, and every biller ties out ​

The constraint: three sources of different finality. A provider-reported figure per request exists on one wire only: OpenRouter's usage.cost on every response, streaming included (the final chunk before [DONE]), in credits whose base currency is USD; on a BYOK request cost is OpenRouter's 5 percent fee and cost_details.upstream_inference_cost the vendor's charge. OpenAI and Anthropic report tokens, never dollars. A rate card exists for 1,354 models in pi's registry (USD per million, with tiers such as GPT-5.4's step above 272,000 input tokens) and in the operator's costs.prices. An invoice exists per biller: Anthropic's cost report (daily; the costs spec records that it closes hours after midnight), OpenAI's GET /organization/costs (daily, admin key, lag undocumented), OpenRouter's GET /api/v1/activity (daily per model and endpoint, a management key, 30 days, usage and byok_usage_inference apart).

The design: the meter row is the model.turn span with its four counters plus biller, vendor, usd, priceSource and, on a BYOK turn, feeUsd (the aggregator's own charge, so the vendor's part is usd less the fee). Precedence per turn: provider when the response carries a cost (cost, plus the upstream cost when is_byok); else operator when costs.prices names the exact ref; else registry when the card has a cost, tiers applied by pi's rule; else none, unpriced, never $0. The Anthropic list the estimate uses today folds into the registry layer with its family rule kept. A run's cost and the costs page sum usd where every turn has one and say unpriced otherwise, as now. Tie-out generalizes from one biller to each: LlmCostSource becomes one source per block that names an invoice API and its own key variable (invoiceKeyEnv; the Anthropic admin key moves under the anthropic block), and the daily tie-out compares that biller's invoice with the summed rows whose biller is that block; for an aggregator with BYOK turns, its usage column against the summed feeUsd and its byok_usage_inference column against the summed remainder; a biller with no invoice source ties out against nothing and the page says so.

Invariants: usd is never a sum across sources; priceSource names exactly one; a provider figure is stored as reported, never re-priced from a card; a tie-out row names the biller and the day. Failure mode: a stream that breaks before its final chunk carries no counters either, so the turn is unmetered as today and its price none. The alternative it beat: pricing every turn from the registry and reconciling to invoices. It leaves the one biller that hands us the exact figure list-priced, and an aggregator's list is a fiction across its providers and its time-of-day overrides.

Grammar: the block declares its biller, vendor and catalog ​

A model ref is <block>/<model-id>, split at the first slash, as today; the block is the biller. The block declares vendor: a name (default, the block's own), or model, which makes the block an aggregator whose vendor is the id's first segment. It declares catalog: the pi registry file consulted for its cards (default, the block's own name when such a file exists; none otherwise), so a block named or still reads openrouter.json and a vercel-ai-gateway block reads its 237 cards. A card is looked up by id under the block's wire first (the registry spells our openai-chat as openai-completions), then under any wire, since a card of another wire still carries levels, window and price. One pure vendorOf(ref, blocks) replaces the eight splits; parseModelRef keeps its shape and is the only other parser. type becomes wire with three values; type: anthropic and type: openai-compatible keep loading as anthropic-messages and openai-chat for one release, then are refused by name. A block gains models.<id> for overrides (levels, capField, window, inputs, cache, price) and passthrough, a map merged into the body on the wires whose adapter takes one (pi's samplingParams on the OpenAI wires; OpenCode's body overlay), where a vendor-only feature lives: OpenAI's service_tier, OpenRouter's provider routing, reasoning.summary, verbosity. A passthrough is never a control: it is not decided, not noted and not in the matrix, and a wrong one surfaces as the provider's 400 on the first turn, as any unknown body field does today.

The alternative it beat: inferring the aggregator from the URL or the name, as pi does. pi keys the cache markers on a provider named openrouter and the routing format on the URL, and a run's provider is the proxy, so inference is exactly how the run lost its markers.

Wires: one route per wire, a gateway is a block, and the Responses route is structural ​

The proxy keeps one route per wire and pins the same two fields. The third route, /v1/responses, pins model and max_output_tokens (floor 16) and meters input_tokens, output_tokens, input_tokens_details.cached_tokens and cache_write_tokens, output_tokens_details.reasoning_tokens. It is structural: every first-party OpenAI card in pi's registry is openai-responses, OpenAI recommends the wire for new projects, and its chat route has no tool calling with effort above none from GPT-5.4 and none on GPT-6 Astra. The card cannot refuse that cell, since no layer knows a model's generation on a wire whose registry has no card; the example config's openai block therefore moves to wire: openai-responses, and an operator who keeps openai-chat on OpenAI meets the provider's own error on the first turn, as today. Both harnesses speak the wire: pi's adapter (with service_tier and prompt_cache_options) and OpenCode's bundled @ai-sdk/openai package (packages/core/src/aisdk-native.ts:100-111 at 2.0.3, beside the chat or responses choice it spells for Azure and Bedrock); the harness write unit confirms the default endpoint against a logging fake, as it did for the two routes today.

An LLM gateway in front of the providers (a deployment of Bifrost or LiteLLM, which speak an OpenAI-compatible prefix and an Anthropic Messages prefix) is a provider block, never a replacement for the proxy. The gateway owns what sits behind its virtual key: the vendor keys, fallbacks and weighted routing, hierarchical budgets and rate limits per key, semantic caching and its own request logs. The proxy owns what a gateway cannot know: the run bearer, the pin to the preset's model and cap, the turn guard, the meter row on the run's record and the card itself. A deployment that runs one writes one block per virtual key (wire: openai-chat or openai-responses at the gateway's /v1, anthropic-messages at its /anthropic, vendor and catalog as for any aggregator) and picks the block through the scope ladder like any other ref, so a key per channel or preset is configuration; a deployment without one runs unchanged. A gateway's per-request cost, when its response carries one, is a provider price source, and its logs store an invoice source, both the meter unit's to verify; its single-instance availability is the block's, not the proxy's. The rule for any future wire: a route, a usage reader and a wire default row in the card; never a translation between shapes.

Matrix: the permutations are the suite ​

The rows are controls × outcomes: effort native, degraded by fallback, degraded unvouched, refused; the cap field named and unvouched; tool choice forced to one call on each wire; cache markers, automatic, none; inputs image and document; window known and unknown; price provider, operator, registry, none; a wire the harness cannot speak. Nineteen rows over eight controls. The columns are drivers: a harness (pi over the fake container, OpenCode over the fake of its server; src/core/harness/testing/scenarios.ts holds the seam) on a block (a wire, a vendor, a registry card or none). A cell is a scripted run through the harness seam against a capturing upstream (the proxy's injectable fetch, ModelProxyDeps.fetch, answering a canned stream) and asserts three things: the record (run_meta.card, the control_degraded notes, or the refusal) matches the declared outcome; the captured payload carries the wire word the card mapped, or lacks it; the span's usd and priceSource match. A declared cannot that passes is a failure, as in the harness suite. The lint fails by name when a wire in the enum, a block in the example config, or a control in the enum has no row, on the pattern of the command suite's policyGaps. Nothing waits: every upstream is scripted. Nineteen rows by fewer than ten drivers is of the order of the harness suite's 84 cells, which run in 2.7 seconds today. The live half is providers check, which reads OpenRouter's GET /api/v1/models/{vendor}/{model}/endpoints for each aggregator model the config names and reports where supported_parameters disagrees with the card; its output and U36's paid A/B are the human-gated receipts. The criteria the suite binds, one per row plus the meter, the tie-out, the route and the lint, are the units' verification rows in the program plan's amendment, not this record's.

Why not X ​

Why not put the gateway in charge: point the proxy at it as the one upstream and let it own the catalog, budgets and attribution? It cannot write our run record, so a degradation still goes unsaid; it translates each call, so the harness's own adapter no longer speaks the dialect (thinking payloads, cache markers, tool shapes); its budgets are per virtual key where ours are a lease per run (record 0046); and a self-hosted BYOK deployment must run without it. As a block it gives everything it has and takes nothing the proxy owns.

Why not let pi decide? Its registry has the levels, the windows and the prices, and it already clamps. pi clamps silently at launch; it prices the turn and we discard the figure; OpenCode never reads pi's registry; and we already overwrite pi's card with an invented one, so the bot is the place that decides and merely decides wrong.

Why not read OpenRouter's models API at dispatch? A network read on the dispatch path for every run, and the truth for a direct block is not there. It is the verifier, run as a command.

Why not translate in the proxy, one route for every wire? The proxy's contract is two pinned fields and the rest byte-for-byte; a translator is a fourth adapter maintained against three moving dialects.

Why not our own catalog file? 1,354 cards maintained upstream and pinned with pi, versus a file that rots. The operator's table covers what the registry lacks; the check command says where they disagree.

Why not refuse everything unknown? A local server would never get a run. Degrade with the note, and give the operator the check command and the override.

Boundaries ​

Not in this record: carrying documents to a provider that takes them (a harness unit; every card degrades the document row until then); a user interface beyond the run page, the costs page and the reply; a type: openai block with code behind it. OAuth subscription routes (Codex, Copilot) are rejected by name: a block carries an API key variable, a subscription is a different credential flow and biller, and pi's registry holds those providers for a later record that adds an auth kind. Compaction per model is the window on the card (OpenCode reads it as the model's limit.context; its per-model compaction key is a mode switch, not a threshold); the reserve and kept tokens stay per deployment. Fast and priority modes are passthrough. A configured ref that fails to resolve (an unknown block, a refused tier, a harness that cannot speak the wire) is refused, never substituted, as the unknown block is today. BYOK is the only mode this software has. Compatibility: type loads for one release; the shipped defaults are all Anthropic, so no run in the shipped configuration changes outcome, and the benefit reaches the operator who switches, which is the ask; a run that proceeded on a clamped xhigh proceeds degraded with a note; the only new refusals are a tier a card marks refused and a wire a harness cannot speak. Resolving the card is a lookup in memory (the registry files ship with pi and load once), not a call.

What would change our mind ​

AssumptionCheapest evidenceWhen
pi's registry is right for the models operators pickproviders check against OpenRouter's endpoints for every model in the example configbefore unit one
OpenRouter's usage.cost arrives on every stream, cache writes and the BYOK split included, for an Anthropic vendorU36's paid A/B, both arms, the final chunk loggedbefore the meter unit
OpenCode's variant body overlay reaches the wire unaltered and @ai-sdk/openai defaults to Responsesa logging fake behind the harness write, as the two routes were measuredbefore the OpenCode row
A gateway's response carries a cost and its logs are an invoiceone logged request on the staged deploymentbefore the invoice unit

Reversibility: the card is additive on the record and the config, so every unit before the Responses route backs out by removing the write into the harness and leaving the invented card; readers that predate the new span attrs ignore them.

Rollout ​

U36's ask receipt and A/B first, on the tracker. Then, one unit each, in the amended program plan: the grammar and the block (vendorOf, wire, vendor, catalog, models, passthrough, the card resolved and recorded, the notes and the refusal); the harness write (pi's models.json levels, cap field, window and compat; OpenCode's limit.context and variants); the meter row and the OpenRouter cost read; the invoice sources and the per-biller tie-out; the Responses route; the matrix and providers check land with each unit's rows, never after. Nothing flag-gated: each unit is compatible with the config that runs today.

Open questions ​

QuestionOwnerResolves itBefore
Whether the Anthropic list retires into the registry layer or stays for the open-day estimate, which prices Admin API rows by model idthe meter unit's authorthe estimate's families against the registry's Anthropic filethe meter unit
Registry refresh cadence when pi is bumpedthe maintainerproviders check in verify reporting drift against the config's modelsthe matrix unit
Whether pi grows a compat flag for prompt_cache_key on a foreign URL for the chat wire, or the harness sets long retention on an openai-chat blockthe harness write unit's authoran upstream issue or the retention setting measured against the fakethe OpenAI cache row

Sources ​

  • The handoff issue #1532 and its comment; the program plan's OpenRouter requirements, key decision and unit (U36); U36's tracker issue #1337 (the config half merged in #1334, live in 1.233.0; the ask receipt and the A/B owed); the staged gateway's change in the infrastructure repository (its README and config.json).
  • Record 0032, record 0038, record 0046; specs model-proxy, harness-pi item 13, costs items 4a to 4c and 10, routing-and-config item 2.
  • pi's library at the pin, @earendil-works/pi-ai 0.85.1 and pi-coding-agent 0.85.1: api/openai-completions.js, api/openai-responses.js, api/anthropic-messages.js, models.js, providers/data/*.json, core/provider-composer.js, core/sdk.js, core/compaction/compaction.js.
  • OpenCode at the pin, 2.0.3: packages/core/src/aisdk-native.ts, packages/schema/src/config/provider.ts, packages/schema/src/model.ts.
  • OpenAI, read 2026-09-17: the Responses and Chat Completions references, the reasoning, prompt caching, migrate-to-Responses, function calling and pricing pages, the Usage and Costs admin references.
  • OpenRouter, read 2026-09-17: usage accounting, the generation endpoint, reasoning, prompt caching, provider routing, the models and endpoints references, credits and activity, the Responses endpoint; the public models list for the DeepSeek V4 entries.
  • Anthropic, read 2026-09-17: the Admin usage and cost report references, extended thinking and effort, prompt caching, tool use, pricing.

Appendix: the survey ​

Every fact checked at df468d8f, with its proof. Registry figures are from the installed library at 0.85.1.

AreaFactProof
BlockProviderConfig is type (anthropic or openai-compatible), apiKeyEnv?, baseUrl?; nothing elsesrc/core/provider.ts:96-102
ProxyTwo routes; the body's model and output cap pinned (max_completion_tokens kept when the body used it); everything else byte-for-bytesrc/channels/modelProxy.ts:27-28, 193-206; model-proxy.md item 1
ProxyUpstream per shape: x-api-key on Anthropic, bearer on OpenAI when a key variable is named, none for a local endpoint; fetch injectablemodelProxy.ts:83-84, 217-263, 631
Metermodel.turn attrs: ref, tools, stop reason, four counters, ttft; no pricemodelProxy.ts:489-510
UsageAnthropic reads cache read and creation; OpenAI reads cached_tokens only; a usage without both core counts leaves the turn unmeteredsrc/core/modelProxy/usage.ts:13-36
RefparseModelRef splits at the first slash; five call sites; three by-hand splits in src/ and one in a test driverprovider.ts:108; resolve.ts:209, route.ts:1159, provision.ts:867, runLoop.ts:803, memory/index.ts:110; validate.ts:500; modelPricing.ts:113, 164; opencode/testing/driver.ts:2020
EffortFive tiers validated as words at load and on config set; resolved through the scope laddersrc/effort.ts:9; validate.ts:118-141; routing-and-config.md item 2
Effort, piTier → pi level by identity as the --model <id>:<tier> suffix; the run card says reasoning: true and sets compat.forceAdaptiveThinking by generationpi/process.ts:51-56, 168-170, 192-215, 237-238
Effort, OpenCodeeffort() returns undefined; no variants declaredopencode/harness.ts:111-119
Effort, bot sideRouter and reflection cards say reasoning: false; no thinking field on the Anthropic wire, nothing on the chat wirepiAi.ts:142-156; pi-ai anthropic-messages.js:851, openai-completions.js:727-736
pi clampWith a map, null refuses a level and xhigh/max need an entry; the clamp walks up, then down, at session startpi-ai models.js:550-581; pi-coding-agent core/sdk.js:137
pi wireChat: reasoning_effort (or reasoning: {effort} on an OpenRouter URL), max_completion_tokens for OpenAI, OpenRouter, Groq and unknown URLs (compat.maxTokensField overrides), store: false, prompt_cache_key only for a URL containing api.openai.com or long retention, and then with a session id; Anthropic cache markers through OpenRouter only for provider === "openrouter", or compat.cacheControlFormatpi-ai openai-completions.js:588-607, 693-736, 1245, 1276-1283, 1323-1349
pi pricecalculateCost from the card after every parse; no adapter reads a provider costpi-ai models.js:530-549
pi defaultsA models.json card without a field: reasoning false, contextWindow 128,000, zero cost; compat.maxTokensField acceptedpi-coding-agent core/provider-composer.js:68-76, core/model-config.js:67
Registry1,354 cards in 39 files; openrouter.json 366, vercel-ai-gateway.json 237; cards carry reasoning, thinkingLevelMap, contextWindow, maxTokens, cost with tiers, input, compat; a file is keyed wire, then idpi-ai providers/data/*.json; types.d.ts:702-737
Registry sampleopenrouter.json → deepseek/deepseek-v4-pro: map high, xhigh (low, medium, max null), 1,024,000 / 384,000, 0.89 / 1.78 / 0.074 / 0, text; deepseek.json → deepseek-v4-pro: map high, max, 1,000,000, 0.435 / 0.87 / 0.0036 / 0; no deepseek-v4.1-flash in either; openai.json: 39 of 39 cards on Responses, gpt-5.4 tier above 272,000; anthropic/* in openrouter.json live under anthropic-messages, their :batch twins under openai-completionsproviders/data/openrouter.json, deepseek.json, openai.json
OpenRouter livedeepseek/deepseek-v4.1-flash: created 2026-09-10, context 1,048,576, max output 384,000, base prompt 0.00000015, completion 0.0000006, cache read 0.000000003 USD per token with time-of-day overrides, text and image, supported_parameters with reasoning, reasoning_effort, tools, tool_choice, structured_outputs, max_tokens; without max_completion_tokens or parallel_tool_callsGET https://openrouter.ai/api/v1/models, read 2026-09-17
OpenCodePer-model config: variants: [{id, settings?, headers?, body?}], capabilities.input, cost with a context tier, limit {context, input, output}, compaction as a mode (local, or provider with a threshold); a model ref takes #<variant>; the binary bundles @ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/openai-compatible among its native packagespackages/schema/src/config/provider.ts:22-62, packages/schema/src/model.ts:11-34, 93-97, packages/core/src/aisdk-native.ts:98-121 at 2.0.3
Window200,000 hardcoded for every run model and the bot-side provider; OpenCode's default the samepi/process.ts:240; piAi.ts:68; opencode/process.ts:68, 321
CompactionPer deployment: pi.compaction, opencode.compaction; pi triggers at contextWindow − reserveTokens from the cardconfig.example.yaml:370-393; pi-coding-agent core/compaction/compaction.js:160-164
DocumentsText stubs on every path, no typed notepiAi.ts:337-343; pi/harness.ts:225-227; pi/mirror.ts:380-383; pi/relay.ts:386
PriceAnthropic list by family; costs.prices by exact ref, four kinds; modelPriceOf falls back by dropping the prefix; unknown is undefined, never $0modelPricing.ts:23-42, 107-142
Costs pageOne LLM dollar line from the Anthropic cost report per workspace; open days estimated from the hourly usage report; LlmCostSource seam; by-model viewcosts.md items 4, 4a, 4b, 4c, 5, 10; src/channels/costsView.ts:65-73
Recordrun_meta carries model?, harness?, harnessScope?, effort?src/core/runEvents.ts:558-576
OpenRouter blockLive in the example config; switchboard init --openrouter-key keeps it; the comment promises cache markers on a runconfig.example.yaml:27-40; src/setup/plan.ts:191; U36 on #1337
GatewayThe staged gateway serves an OpenAI-compatible /v1 (chat, responses) and an Anthropic /anthropic prefix behind a virtual key (enforce_auth_on_inference), one instance by design, with a Postgres config and logs store and 30-day payload logsthe infrastructure change's bifrost/README.md and config.json
Conformance patternSCENARIOS (34 rows) × drivers; rowVerdict pass, fail, cannot, absent; a declared cannot that passes fails; runRow refuses a row that reads no record; 84 cells in 2.7 ssrc/core/harness/testing/scenarios.ts:439, 1227, 1254-1323; src/core/harness/conformance.test.ts
OpenAIResponses recommended; chat loses tool calling with effort above none from GPT-5.4; effort enum none…max per model; max_tokens deprecated; caching automatic, 1,024-token minimum on GPT-5.6 and later, cache writes 1.25× and reported; prompt_cache_key replaces user; service_tier incl. fast; Costs API daily, Usage API 1m/1h/1d by model and api key; max_output_tokens minimum 16the OpenAI research, read 2026-09-17
OpenRouterusage.cost always returned, streaming in the final chunk; cost_details.upstream_inference_cost, is_byok, cache_write_tokens; reasoning effort seven values; per-vendor cache rules; require_parameters (default false: unknown parameters ignored); models API pricing as USD-per-token strings with supported_parameters; endpoints API per provider; /api/v1/activity daily with usage and byok_usage_inference, management key; BYOK 5 percent; /api/v1/responses statelessthe OpenRouter research, read 2026-09-17
Anthropicoutput_config.effort five values, xhigh on the newest models only; budget_tokens rejected from 4.7; cache_control ttl 5m or 1h; usage splits 5m and 1h writes; 1M context at standard rates on 4.6 and later; Admin usage report 1m/1h/1d by model, api key, service tier, context window; cost report dailythe Anthropic research, read 2026-09-17

Amended 2026-09-18 — the harness write names the biller's own provider, never a generic alias ​

Re-evaluation, written while the record is still proposed, because the first production measurement contradicted a sentence in it. The record's Grammar section rejected inferring the aggregator "as pi does" and chose to declare vendor and catalog on the block, with the harness write carrying each control from the card into the harness's own knobs (compat.cacheControlFormat for pi). That choice stands: the card is still the one place a control is decided. What the measurement showed is that the harness write's other half — under which provider identity the harness is told to speak — was left implicit, and "the proxy, as a generic OpenAI-compatible endpoint" is an identity under which neither harness can honour the card.

The measurement. The five-task comparison ran the same model through the proxy on both arms, on OpenCode: the aggregator arm sent every turn uncached (the aggregator's own per-minute analytics show cached_tokens: 0 across all 275 requests in the window, each billed at list) while the direct arm read most of its prompt from cache; the same work cost about eight times as much on the aggregator arm. The run records on the aggregator arm carried card.cache: markers, vendor: anthropic, and no control_degraded note for cache — the card was right and vouched, and nothing in the harness could act on it.

Why. OpenCode at the version the resident runs decides caching by wire protocol, and chooses the protocol from the provider package: @ai-sdk/openai-compatible with a base URL maps to its generic chat protocol, for which the cache-policy pass is skipped entirely; @openrouter/ai-sdk-provider maps to its own aggregator protocol, which places up to four cache breakpoints (last tool, first and last system part, the conversation tail), asks the aggregator for usage.cost on the final chunk by default, and takes a baseURL override — so it works unchanged through the proxy. There is no knob on the generic protocol that would carry the card's markers. pi keys four behaviours on the same identity (provider === "openrouter" or a base URL on the aggregator's host): the developer role for its Anthropic and OpenAI ids, the reasoning: { effort } shape instead of reasoning_effort, session affinity, and the Anthropic cache-control format; the harness write sets one of the four from the card today and leaves three at the generic defaults.

What changes. The harness write chooses the harness's own provider for the block's biller, with the proxy as that provider's base URL, and then carries the card into that provider's knobs: on OpenCode the package (@openrouter/ai-sdk-provider for an aggregator block whose biller is that aggregator, @ai-sdk/anthropic on the Anthropic wire, @ai-sdk/openai on the Responses wire, @ai-sdk/openai-compatible only for a block that is generic in fact); on pi the full set of compat words the biller implies (cacheControlFormat, thinkingFormat, sessionAffinityFormat, supportsDeveloperRole) beside the card's own (maxTokensField, the level map, the window). The block therefore declares which harness-side provider it is served by — the same declaration the record already makes for vendor and catalog, extended to the harness — and a block whose biller has no harness-side provider is served generically and its cache row is degraded with a note, never silently native.

What does not regress. The record's four success criteria hold: no code path outside the card's tables names a provider (the biller-to-package map is a table on the block type, read by the harness write); the controls still resolve once, before the first call; the matrix keeps one cell per harness × block × control, and the cache cell grows a second assertion. The matrix's cache row for the aggregator × OpenCode cell had passed against the fake of OpenCode's server, which cannot run OpenCode's protocol choice; under record 0038's fidelity rule that cell is measured with the real binary against the logging fake before it is called native, and the record's Matrix section is read with that gate on every harness row from here.

What stays open. Whether an Anthropic model through the aggregator is better served on the Anthropic wire — the aggregator exposes a Messages endpoint, pi's aggregator provider speaks it, and OpenCode's Anthropic protocol would place native markers and read native cache counters — is the aggregator unit's author's to measure against the same fake; the Chat wire with the aggregator's own protocol is the first cell because it serves every vendor behind the aggregator, not only one.