Chapter 12 of 30

One Operation, Several Model APIs

Concepts

CHAPTER 12 β€” One Operation, Several Model APIs

STATUS

Full draft, rewritten 2026-09-13 from executed CodeAI Stage 12 evidence. Replaces the first-pass draft built on Writer’s three clients (Writer is no longer the construction spine for this chapter).

EDITORIAL PASS (2026-09-14)

  • Visual-mechanism pass 2026-09-14: added one-operation to three-adapters to stable-contract flowchart (renamed/nested vs omitted-recorded vs defaulted-recorded). Matches the six-fates section.
  • Closure pass 2026-09-14: added synthetic probe table (InvalidControlError/UnknownControlError pre-effect refusals vs recorded omission); names and tests verified against source. Labeled synthetic per chapter convention.
  • Weaknesses #1 and #2 re-checked against current CodeAI interpretation.decide_attempt: default ATTEMPT_POLICY_V2 still accepts output with no error and generation_state unknown (unknown reason -> succeeded), and still retries EMPTY generation (tool-only reply retried). POLICY_V2_RETRYABLE dropped empty_output as an error-kind, but EMPTY state is retried directly. Chapter now states both still hold by default.
  • Process note removed (“The rule in the book’s preparation notes”).
  • Spelling: catalogue -> catalog; labelled -> labeled; unrecognised -> unrecognized.
  • Figures re-verified against protocol-conformance README (synthetic 24/24; offline 27/27, network blocked; live 3 routes HTTP 200, one attempt).
  • Score ~930 -> ~950. Remaining ceiling: prepare/send code is a labeled simplification; evidence is conformance-level, not an experiment on model behavior (by design).

CENTRAL QUESTION

How does one logical operation survive a change of wire dialect, without the runtime knowing, and without hiding anything that matters?

THESIS

The adapter’s job is to CONTAIN differences, not hide them: map what is equivalent, record what is omitted or defaulted, refuse what it does not understand, and prove the runtime decides identically across dialects.

SECTION OUTLINE

  • Right model, wrong dialect: Ch 11 run 2; OpenCode Go serves each model on exactly one dialect, so choosing an occupant chooses a protocol (Ch 10 callback).
  • One swap, many changes: 3-route difference table (endpoint, input field, limit field, headers, reasoning control fate, default, finish signal, reasoning location, usage vocabulary, Go usage cap).
  • Six fates of a control: sent / renamed / nested / omitted+recorded / defaulted+recorded / refused pre-effect. RFC 9413 against tolerance; Sculley configuration debt; CodeAI’s own pre-11.5c defects.
  • Send what you record: prepare once -> manifest records it -> send transmits it -> retries resend it. Hash is canonical semantic JSON, not wire bytes. Parnas: dialect is the decision likely to change. runtime.py has no protocol names.
  • Read the answer where it lands: text location per dialect; reasoning kept in preserved bytes, never in the answer; versioned completion map.
  • The same prompt is not the same input: live usage table; tokens do not transfer across routes; normalize equivalent / preserve non-equivalent / never force equivalence; latent Messages cache trap.
  • The experiment in two layers: offline (27/27, sockets refused, identical outcome matrix across dialects) and live (route executability only; models differ; budget cap untrippable).
  • Where it is still weak: seven named gaps.
  • Do this now: six-fate table, malformed-control probe, hand-parse, swap count; reader build prompt.

LOAD-BEARING CLAIMS

  1. On this gateway a chamber swap is a protocol swap (docs: one endpoint per model). [measured: docs]
  2. Every control meets one of six fates; fates 5 and 6 are where systems lie. [argued]
  3. Silent omission and silent defaults are hidden variables in every comparison. [argued]
  4. Refuse unknown controls before any effect (RFC 9413 applied to codecs). [argued + inspected tests]
  5. Prepare once: the recorded request and the sent request are the same object. [inspected tests]
  6. The dialect does not reach the decision: identical outcome per case across all three dialects. [measured]
  7. Tokens are not a cross-route unit; provider usage breakdowns are observations, not facts. [measured]
  8. Live multi-route comparisons cannot isolate a protocol effect on this gateway. [argued from docs]

EVIDENCE (all independently re-verified in this pass)

  • CodeAI HEAD cfd02e0 (Messages 247f2ab; harness fixes c642cff; verifier cfd02e0). Full suite 234 passed; ruff clean (run twice this pass).
  • Bundle: experiments/applied-ai/evidence/protocol-conformance/
    • synthetic/: 24/24 (3 dialects x 8 cases). verify_protocol_conformance.py PASS (95 hashes + chains).
    • offline/: 27/27 incl. 3 captured, network_calls 0, manifest pins 247f2ab + per-file source hashes. PASS (107 hashes + chains). Captured body hashes equal live capture hashes.
    • live/: 3 routes, 1 attempt each, HTTP 200, generation complete. PASS (21 hashes + chains).
    • Outcome matrix (computed this pass from offline report): each of 8 cases -> exactly 1 distinct (call_status, generation, transport, error_kind) across 3 dialects: complete succeeded / complete / response_received / - truncated unresolved/ truncated / response_received / - unknown succeeded / unknown / response_received / - empty failed / empty / response_received / empty_output tool_only failed / empty / response_received / empty_output http_error failed / unknown / http_error / invalid_request malformed failed / unknown / response_received / malformed_response no_resp failed / unknown / no_response / timeout
    • Semantic request rows (offline ‘complete’): responses effective {temperature .2, max_output_tokens 256, reasoning {effort low}}, omitted []; chat {temperature .2, max_tokens 256}, omitted [reasoning_effort]; messages {temperature .2, max_tokens 256}, omitted [reasoning_effort]; plan versions opencode-request-plan-v1 / opencode-messages-request-plan-v1.
    • Live usage (from response.bin): responses/gpt-5.6-luna 38 in / 26 out, cached 0, reasoning 0, total 64, status completed, visible 125 chars. chat/mimo-v2.5 prompt 279 / completion 166, cached 192, reasoning 0, total 445, finish stop, content 112 chars, message.reasoning 780 chars. messages/minimax-m2.7 73 / 220, no cache/reasoning fields, stop_reason end_turn, thinking block 1,094 chars, text 74 chars.
    • Live observations: headers {} for all three; observation.provider_request_id None; body ids resp_0df1…, gen-1789311880-…, 06f5ee91… .
    • Independent checks: independent-expectation-check.json (other session) and independent-verification/live.json from verify_captures.py (this pass; no CodeAI imports): text and completion agree 3/3; secret-pattern hits 0.
    • Budget: max_calls 3, max_cost_usd 0.5 nominal, allow_unknown_cost true with recorded reason. The cost ceiling cannot trip when per-call cost is unknown; calls + token limits were the real bound.
  • Tests inspected: test_invalid_values_rejected_before_effect, test_unknown_control_rejected_pre_effect_no_invocation, test_structural_model_override_rejected, test_secret_parameter_rejected_never_persisted, test_sent_body_equals_prepared_body_exactly, test_retry_resends_same_prepared_request (test_request_plan.py); test_messages_codec.py (defaults, omissions, completion states).
  • Inspected: runtime.py has no protocol-name references (grep). interpretation.py _COMPLETION_REASONS, POLICY_V1_RETRYABLE = {transient_failure, empty_output, timeout, rate_limited}.
  • 11.5c control audit (commit 9ef0599 message, implementing agent’s report β€” cited as “the audit recorded with the stage”, backed by the tests above): max_tokens “abc” omitted from body yet recorded as sent; chat reasoning_effort accepted, never sent, invisible.
  • OpenCode Go docs (fetched twice 2026-09-13; page “last updated September 11, 2026”): gpt-5.6-luna on /v1/responses; mimo-v2.5 on /v1/chat/completions; minimax-m2.7 on /v1/messages; session header x-opencode-session documented; no anthropic-version / x-api-key mentioned; per-model monthly caps GPT 5.6 Luna $15, MiMo-V2.5 $60, MiniMax M2.7 $60; subscription $10/month.

PAPERS / SOURCES

  • Parnas, CACM 15(12):1053-1058, 1972. Hide the design decisions likely to change inside modules.
  • Thomson & Schinazi, RFC 9413 “Maintaining Robust Protocols”, Informational, June 2023 (VERIFIED). One quote used (12 words). Argues tolerance of unexpected input causes long-term decay; recommends active maintenance.
  • Sculley et al., NIPS 28, 2015 “Hidden Technical Debt in ML Systems” (VERIFIED abstract): boundary erosion, entanglement, hidden feedback loops, undeclared consumers, data dependencies, configuration issues, changes in the external world, system-level anti-patterns.
  • Dropped from first-pass draft: Gorilla (Patil et al.) and HELM (Liang et al.) β€” not load-bearing here.

CONTROLS / LIMITATIONS

Live layer: three different models, so no protocol effect is identified; one question, one call per route, one date. Offline layer: synthetic negatives labelled synthetic; three real captures only; the harness prompt is a one-sentence code-comment review, not paragraph P. Request hash = canonical semantic JSON, not outbound bytes. “Most of the Chat route’s input was its own” is an inference from cached 192 of 279 on a first send. The Messages cache-convention mismatch is latent (no cache fields in this capture) and attributed to the Anthropic Messages documented convention. Go caps/routes are dated external facts; recheck before publication.

THE SIX FATES

sent | renamed | nested | omitted + recorded | defaulted + recorded | refused before any effect

THE SEVEN WEAKNESSES

1 unknown completion reason -> call succeeded 2 tool-call-only -> empty_output -> retryable 3 headers {} and provider_request_id None on all live captures (ids only in bodies) 4 canonical usage = input/output only; latent Messages cache mismatch 5 semantic hash, not wire bytes 6 Messages credential sent in two headers; docs name neither 7 one question / one call per route / one day; catalogue changed two days earlier

DEPENDENCIES

Ch 6 (unknown cost, budgets), Ch 7 (evals as experiments), Ch 9 (most-available routing needs headers), Ch 10 (chambers, release protocol, cost per passing item), Ch 11 (recorded call, run 2, manifest-before-effect).

FORWARD BRIDGE

279 vs 38 input tokens, 192 cached, reasoning_tokens 0 beside 780 chars of reasoning: the observation is preserved; its meaning is unsettled. Interpretation must be versioned and unable to rewrite observation (Ch 13).

OPEN ITEMS CREATED BY THIS CHAPTER

  • Root .gitignore (uncommitted, other session) lists every live/ evidence file, which would keep the only real captures out of version control. Flagged to the author; not changed in this pass.
  • verify_captures.py and independent-verification/live.json were added to the bundle in this pass; they overlap independent-expectation-check.json but add usage, ignored-block and secret-scan detail cited above.
  • Reader prompt pack has no Chapter 12 increment; the chapter carries its build prompt inline.
  • Ch 13 should re-read the three live usage blocks under a versioned usage interpretation.
  • Ch 22 should revisit tool-only-as-retryable; Ch 28 needs headers for availability routing.

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Part 2 β€” Get the Model Out of the Chat Box

Right model, wrong dialect

Chapter 11’s second live run failed with an HTTP 500. The gateway, the model, and the credential were all right. The request was shaped for OpenCode’s Responses endpoint, and mimo-v2.5 is served on Chat Completions.

That looks like a configuration slip. It is a property of the ground you are building on.

OpenCode Go’s catalog, updated on 11 September 2026, lists every model on exactly one of three endpoints (OpenCode). Three of them carry this chapter:

Model Endpoint API family
gpt-5.6-luna /zen/go/v1/responses OpenAI Responses
mimo-v2.5 /zen/go/v1/chat/completions OpenAI Chat Completions
minimax-m2.7 /zen/go/v1/messages Anthropic Messages

On this gateway, choosing a model is choosing a protocol. Chapter 10 made the occupant of each chamber replaceable, which means every chamber swap is also a protocol swap, whether or not anyone intended one.

How does one logical operation survive a change of wire dialect, without the runtime knowing, and without hiding anything that matters?

The textbook answer is an adapter that hides the differences. That is half right. The part it gets wrong is the word hides. An adapter that hides everything also hides the differences that change your results. The job is to contain them: translate what is equivalent, record what is not, and refuse what it does not understand.

One swap, many changes

Here is what actually differs between the three routes, all on one gateway under one subscription.

gpt-5.6-luna mimo-v2.5 minimax-m2.7
Endpoint /v1/responses /v1/chat/completions /v1/messages
Prompt goes in input messages messages
Output limit field max_output_tokens max_tokens max_tokens, required
Extra header β€” β€” anthropic-version
A requested reasoning_effort sent, as reasoning.effort omitted, and recorded omitted, and recorded
When no limit is requested none sent none sent CodeAI sends 1,024, recorded as defaulted
Where “finished” is reported status, incomplete_details.reason choices[0].finish_reason stop_reason
Where hidden reasoning comes back reasoning output items message.reasoning a thinking content block
Usage vocabulary input_tokens / output_tokens prompt_tokens / completion_tokens input_tokens / output_tokens
Monthly usage cap in the Go plan $15 $60 $60

Chapter 10’s release protocol said: change one thing, measure, then decide. On this gateway you cannot change the model without also changing the endpoint, request shape, headers, the fate of at least one control, the completion vocabulary, the place reasoning comes back and the usage vocabulary. Sometimes the quota headroom changes too.

Two consequences follow, and the rest of the chapter is about both. First, the runtime must not care which row it is on, or every release becomes a rewrite. Second, a live comparison between two occupants can never tell you what the protocol did, because the protocol never changes alone. That is why the experiment at the end of this chapter has two layers.

One operation in, three dialects out, one contract back:

    flowchart TD
    OP["one logical operation<br/><i>review P, max 256 tokens</i>"] --> AR["adapter: Responses"]
    OP --> AC["adapter: Chat"]
    OP --> AM["adapter: Messages"]
    AR --> R1["renamed Β· nested"]
    AC --> R2["omitted + recorded"]
    AM --> R3["defaulted + recorded"]
    R1 --> CT["stable application contract<br/><i>same call, same record shape</i>"]
    R2 --> CT
    R3 --> CT
  

Six fates of a control

Ask all three routes for the same thing: max_tokens 256, temperature 0.2, reasoning_effort “low”. This is what CodeAI recorded as actually sent, from its offline semantic-request cases:

requested    {max_tokens: 256, temperature: 0.2, reasoning_effort: low}

responses    sent {max_output_tokens: 256, temperature: 0.2, reasoning: {effort: low}}
chat         sent {max_tokens: 256, temperature: 0.2}   omitted_unsupported: [reasoning_effort]
messages     sent {max_tokens: 256, temperature: 0.2}   omitted_unsupported: [reasoning_effort]

Every control a caller asks for meets one of six fates:

  1. Sent as is. temperature, on all three routes.
  2. Renamed. max_tokens becomes max_output_tokens on Responses.
  3. Nested. reasoning_effort becomes reasoning.effort on Responses.
  4. Omitted, with a record. reasoning_effort on Chat and Messages, and seed everywhere.
  5. Defaulted, with a record. Messages requires an output limit. If the caller supplies none, CodeAI sends 1,024 and writes that into defaulted_controls.
  6. Refused before any effect. An unknown control name, a malformed value, an attempt to override the model through parameters, or a credential passed as a parameter all raise before the adapter is invoked.

The first four are ordinary translation. Fates five and six are where systems quietly lie.

A default nobody records is a control the caller never chose and cannot see. If the Messages route silently supplies an output limit while the Chat route sends none, then any comparison between those two chambers carries a hidden variable. The difference in your results may be the limit, not the model.

Refusal is the unfashionable one. Protocol engineering spent decades under the robustness principle: be liberal in what you accept. RFC 9413 argues that this tolerance does long-term damage. As Thomson and Schinazi put it, “Tolerating unexpected inputs from another implementation might seem logical, even necessary” (Thomson & Schinazi, 2023). Their point is that tolerated deviations accumulate into de facto requirements nobody chose, and they recommend active maintenance in place of silent acceptance. A codec that accepts a control it does not understand, and simply does not send it, is exactly that tolerance.

This is not hypothetical, even inside CodeAI. The control audit recorded with its request-plan stage found two silent failures in the earlier code. A malformed max_tokens value such as "abc" was dropped from the request body while still being recorded as sent. And the Chat route accepted reasoning_effort, never sent it, and left no trace of the omission.

A synthetic probe pins the corrected behavior β€” no network, same three routes, two bad inputs:

Input Old behavior Current behavior
max_tokens: "abc" dropped from the body, still recorded as sent InvalidControlError from prepare(), before any request exists
unknown control top_k2: 4 accepted and silently never sent UnknownControlError from prepare(), with no invocation following
reasoning_effort: low on Chat accepted, never sent, no trace omitted and recorded in omitted_unsupported

The first two rows are refusals, not translations: prepare() in providers.py raises RequestPlanError subclasses before any provider effect, and regression tests pin that no invocation follows. The third row is the contrast worth keeping β€” a declared-but-unsupported control is legitimately omitted, provided the omission is in the record. Refusal and recorded omission are both honest; silent tolerance is the one that corrupts every comparison built on top.

Sculley and colleagues identified configuration as a characteristic source of hidden technical debt in machine learning systems (Sculley et al., 2015). A setting that silently fails to apply, while the record says it applied, is the worst form of that debt: every comparison built on the record inherits the error. Both cases now raise before any request exists, and regression tests pin that (test_invalid_values_rejected_before_effect, test_unknown_control_rejected_pre_effect_no_invocation).

Send what you record

The first of those defects had a structural cause: two code paths produced two views of one request. One path built the body that went over the wire. Another built the “effective parameters” that went into the record. They drifted, and nothing noticed.

The fix is to prepare once and use the result twice:

prepared = adapter.prepare(spec)   # validate, map, default, refuse, all before any effect
manifest = record(prepared)        # intent, written before the first attempt (Chapter 11)
for attempt in attempts:
    reply = adapter.send(prepared) # exactly the object that was recorded

That is simplified, but the shape is real. prepare() returns a PreparedCognitionRequest holding the endpoint, the body, the public headers, and the requested, effective, omitted and defaulted controls. The manifest records that object. send() transmits that object. A retry resends that object. Tests assert that the body sent equals the body prepared, and that a retry resends the identical prepared request.

Two precise limits. The request hash in the manifest is SHA-256 over canonical sorted JSON. It identifies the semantic request, not the outbound bytes, which the transport serializes separately. And credentials never enter the prepared object: they are applied inside send(). For Messages the key goes out as both a bearer Authorization header and x-api-key.

Parnas’s classic criterion for decomposing a system is to hide, inside each module, a design decision that is likely to change (Parnas, 1972). Which dialect a route speaks is such a decision, and on this gateway it changes whenever an occupant does. So it lives inside the adapter. CodeAI’s runtime.py contains no reference to any protocol name. The runtime decides retries, call status and task state from the canonical interpretation, and never asks which endpoint the bytes came from.

Read the answer where it lands

The three live captures show three different places to find the answer, and in two of them the answer is the smallest thing in the response.

  • Responses (gpt-5.6-luna): the text is an output_text part inside a message output item, and completion is status: "completed".
  • Chat Completions (mimo-v2.5): 112 characters of answer in choices[0].message.content, plus 780 characters of reasoning in message.reasoning. Completion is finish_reason: "stop".
  • Messages (minimax-m2.7): a thinking block of 1,094 characters, then a text block of 74 characters. Completion is stop_reason: "end_turn".

The canonical text CodeAI produces contains only the answer. The reasoning is neither folded into it nor thrown away. It remains in the preserved response bytes, stored under their content hash, where a later interpreter or a human can read it. Merging reasoning into the answer would make the answer wrong; deleting it would destroy an observation. The adapter does neither.

Completion signals arrive in three vocabularies and map onto one set of states. The mapping carries its own version, so it can be corrected later without rewriting history (Chapter 13):

Dialect signal Canonical generation state
stop, end_turn, completed complete
length, max_tokens, max_output_tokens truncated
content_filter filtered
anything else unknown

The same prompt is not the same input

Every live call sent the same one-sentence request. Here is what each route reported:

Route Input tokens Output tokens Visible answer Other usage fields
Responses / gpt-5.6-luna 38 26 125 chars cached 0, reasoning 0, total 64
Chat / mimo-v2.5 279 166 112 chars cached 192, reasoning 0, total 445
Messages / minimax-m2.7 73 220 74 chars none reported

Three facts sit in that table, and each breaks a comparison people routinely make.

Identical requests produced input counts from 38 to 279. Tokenizers differ, and routes can add material you did not send. The Chat route reported 192 of its 279 input tokens as served from cache. Chapter 11’s truncated call, a different request to the same route on an earlier day, also reported exactly 192 cached tokens. Two different requests sharing an identically sized cached prefix suggests the prefix was supplied by the route rather than by us. The record makes that likely; it does not prove it.

Output tokens mostly paid for text you cannot see. Answers of 112 and 74 characters cost 166 and 220 output tokens. The obvious candidate for the difference is the reasoning returned beside them, but neither route’s usage attributed those tokens to it. MiMo’s usage reports reasoning_tokens: 0 while returning 780 characters of reasoning. A provider’s own accounting breakdown is an observation to be interpreted, not a fact to be believed.

One route gave no breakdown at all. Messages reported input and output, and nothing about cache or reasoning.

So tokens are not a unit that transfers across routes. That is the concrete reason Chapter 10 defined a chamber’s utility as cost per passing item rather than cost per token. On a per-token basis these three routes cannot even be placed on one axis.

CodeAI’s canonical usage today carries input and output only, labeled measured. Everything else stays in the preserved bytes. The rule to follow is short: normalize equivalent semantics, preserve non-equivalent semantics, never force equivalence.

There is a latent trap too. In Chat usage, cached_tokens is a detail inside prompt_tokens. In the Anthropic Messages convention, cache reads and cache creation are separate components reported beside input_tokens. CodeAI’s Messages codec reads only input_tokens. This capture reported no cache fields, so nothing was undercounted here. But the first time a Messages route serves from cache, one canonical field will quietly mean two different things. Deciding what these numbers mean is Chapter 13’s job.

The experiment, in two layers

A live comparison cannot isolate the protocol, because no model on this gateway is offered in two dialects. So the evidence comes in two layers that answer different questions, and they are reported separately and never pooled. The full bundle is preserved under experiments/applied-ai/evidence/protocol-conformance/.

Offline: does the boundary hold?

The harness runs each dialect through the real CodeAI runtime with a patched transport and outbound sockets refused. Eight cases per dialect are synthetic, and labeled as such: a complete answer, a truncated one, an unrecognized completion reason, an empty body, a tool call with no text, an HTTP error, a malformed body, and no response at all. Three more replay the real response bytes captured by the live layer. The captured bodies replay byte for byte, with the same hashes as the originals.

27 of 27 cases passed, with zero network calls. The result that matters is not the pass count. It is this table:

Case Transport Generation Error kind Call status
complete response received complete β€” succeeded
truncated response received truncated β€” unresolved
unknown reason response received unknown β€” succeeded
empty response received empty empty_output failed
tool call only response received empty empty_output failed
HTTP error HTTP error unknown invalid_request failed
malformed body response received unknown malformed_response failed
no response no response unknown timeout failed

For every case, all three dialects produced exactly this outcome, with no variation. That identity is the claim: the dialect does not reach the decision. Transport, generation and call status are decided from the canonical interpretation, whatever shape the bytes had.

Two checks guard against fooling ourselves. The expectations for the captured fixtures were first copied from the codec’s own output, which would have made replaying them circular. So the bundle includes independent parses of the raw bytes, written with no CodeAI imports. They agree with the codec on the text and the completion signal for all three captures, and a separate scan found no credential material in the bundle.

Live: does each route actually execute?

Three calls, one per route: one attempt each, a 60-second timeout, and an output limit of 1,024 tokens. A budget guard allowed at most three calls and required a recorded reason for proceeding with unknown cost. All three returned HTTP 200 with complete generation, and all three answers named the missing benchmark or measurement.

That supports exactly one claim: each configured route executed once, on 13 September 2026. It says nothing about protocols, because the models differ. No winner was computed and none should be.

The budget detail is Chapter 6 arriving in practice. The guard was configured with a $0.50 cost ceiling, but on a subscription the cost of a single call is unknown, so that ceiling could never trip. What actually bounded the experiment was the call count and the token limit. Budgets you cannot measure are not budgets.

One more detail: the harness asked a one-sentence question about a code comment rather than reviewing paragraph P, to keep live output short. The operation under test is the boundary, not the review.

Where it is still weak

The boundary holds for what was tested. These are the places an honest reader of the code and the bundle will find gaps:

  1. An unknown completion reason becomes success. Truncation now correctly leaves a call unresolved. An unrecognized reason, the most likely shape of a future API change, still produces a succeeded call. The attempt policy CodeAI uses by default today keeps that behavior: output with no error and an unknown generation state is accepted.
  2. A tool-call-only reply is classified as empty_output, which the retry policy treats as retryable. For a text-only chamber that is defensible. For a chamber whose occupant legitimately answers with a tool call, the runtime will retry a valid answer. The current default policy still retries empty generation.
  3. No response headers survived. Every live capture recorded an empty header map. Request identifiers exist only inside the bodies (resp_…, gen-…, and a bare hexadecimal id) while the observation’s provider_request_id is empty. Rate-limit and retry-after headers, the raw material of Chapter 9’s “most available” routing, were not captured.
  4. Canonical usage is two numbers. Cache, reasoning and totals stay in the raw bytes, and the Messages cache convention is a latent mismatch.
  5. The request hash is semantic, not byte-level. It proves which request object was intended, not which bytes left the machine.
  6. The Messages route sends the credential twice. The route documentation names neither header. The successful call shows the combination is accepted, not which header is required.
  7. One question, one call per route, one day. “Executed today” is not “available tomorrow”, and the catalog these calls relied on had been updated two days earlier.

None of these weakens the central result. Each is a place where the contract is narrower than it may look.

Do this now

Forty-five minutes. Find out what your controls actually do.

  1. For each chamber occupant you use, write the six-fate table: for every control you pass, is it sent, renamed, nested, omitted, defaulted, or refused? If you cannot answer for one control, that control is currently unobserved.
  2. Pass a deliberately malformed value, such as a string where an output limit belongs. Does anything raise before a request is sent, and does your record still claim the value was used?
  3. Take one real response you have from each dialect you use, parse it by hand without your codec, and compare the text and completion reason with what your code recorded.
  4. Answer in writing: if you swapped your most important chamber’s occupant today, how many of the rows in “One swap, many changes” would change?

If you are building with an assistant, this is the increment:

Keep one logical model operation stable across several wire dialects.
Inspect first: list every control the code accepts and what each dialect does
with it. Then implement, reusing the existing call/attempt records:
- one prepare step per call that validates, maps, defaults and refuses,
  producing a single request object that is both recorded and sent;
- record requested, effective, omitted and defaulted controls separately;
- reject unknown or malformed controls before any request exists;
- extract canonical text per dialect; keep reasoning and tool blocks in the
  preserved response, never in the answer;
- map each dialect's finish/stop signal to complete/truncated/filtered/unknown
  under a versioned map.
Prove it offline: the same eight cases through every dialect must produce
identical transport, generation and call outcomes, with outbound sockets
refused. Do not compare model quality across dialects.

Failure modes

  • Treating an occupant swap as one change. On a real gateway it changes protocol, headers, controls, vocabularies and quotas at once.
  • Silent omission. A control that is accepted, not sent, and not recorded.
  • Silent defaults. A limit the caller never chose becomes a hidden variable in every comparison.
  • Tolerating what you do not understand. Unknown controls accepted “to be safe” become requirements nobody chose.
  • Two code paths for one request. The record and the wire drift apart unnoticed.
  • Folding reasoning into the answer, or deleting it. One corrupts the result; the other destroys an observation.
  • Comparing tokens across routes. The same request was 38, 73 or 279 input tokens depending on where it went.
  • Believing a provider’s usage breakdown. reasoning_tokens: 0 arrived beside 780 characters of reasoning.
  • Reading a live multi-route comparison as a protocol effect. The model changed too.
  • Branching on protocol in the runtime. Every new dialect then becomes a runtime change.

What this chapter established

  • On a real gateway, each model is served on one dialect, so choosing an occupant chooses a protocol, and a chamber swap changes many things at once.
  • The adapter’s job is to contain differences, not hide them. Every control meets one of six fates: sent, renamed, nested, omitted and recorded, defaulted and recorded, or refused before any effect.
  • Silent tolerance is debt. RFC 9413’s case against liberal acceptance and Sculley et al.’s configuration debt both appear in CodeAI’s own history: a malformed limit recorded as sent, and an unsent control with no record. Both now fail before a request exists.
  • Prepare once, record it, send it: the same prepared object drives the manifest, the transport and every retry. Its hash identifies the semantic request, not the wire bytes.
  • The runtime contains no protocol branch. Dialect knowledge lives in the adapter, which is Parnas’s criterion applied to the part of this system most likely to change.
  • Answers arrive in three places, and hidden reasoning in three more. The canonical text holds only the answer; reasoning stays in the preserved bytes.
  • The same request was reported as 38, 279 and 73 input tokens, output tokens mostly paid for invisible reasoning, and one route reported zero reasoning tokens beside a reasoning field. Tokens do not transfer across routes, and non-equivalent usage must be preserved, not forced.
  • Offline, 27 of 27 cases passed with the network refused, and every case produced an identical outcome across all three dialects. Live, three routes executed once each. The layers support different claims and are never pooled.
  • Still weak: unknown completion counts as success, tool-only replies are retried, no headers or request ids were captured, usage is two numbers, and the hash is semantic.

Next

The Chat route reported 279 input tokens for a request another route counted as 38, said 192 of them were cached, and reported zero reasoning tokens while returning 780 characters of reasoning. The observation is preserved exactly. What it means is not settled: whether cached tokens are part of the input or beside it, whether “zero reasoning” can be believed, whether two routes’ “input” can ever be added up.

Whoever settles that will get it wrong at least once, as Chapter 11’s classifier did. So the interpretation has to be versioned, replaceable, and unable to rewrite the observation it came from.

Continue with Normalize at the Boundary.

References

Implementation sources: CodeAI β€” 247f2ab (Messages codec, request-plan identity, protocol conformance harness), c642cff and cfd02e0 (harness and verifier fixes), 9ef0599 (prepare/send seam and control audit), 2dd1e31 (versioned interpretation). Symbols: PreparedCognitionRequest, OpenCodeCognitionAdapter.prepare and .send, OPENCODE_ENDPOINTS, _messages_text, _COMPLETION_REASONS, POLICY_V1_RETRYABLE. Tests: tests/test_request_plan.py, tests/test_messages_codec.py. Evidence: experiments/applied-ai/evidence/protocol-conformance/ (synthetic/, offline/, live/, independent checks).