0037: llm-provider — Anthropic Messages Wire-Format Mapping (§8.2)¶
- Status: Accepted
- Author: Chris Colinsky
- Created: 2026-05-27
- Accepted: 2026-05-27
- Targets: spec/llm-provider/spec.md (new §8.2 Anthropic Messages mapping subsection following the §8.X template locked in by proposal 0026; §3.1 Content blocks expanded with two new assistant-message block types —
ThinkingBlockandRedactedThinkingBlock— surfacing provider-emitted reasoning content as first-class spec records; §8.1 OpenAI-compatible mapping extended with a strip-on-send rule for the new block types since OpenAI Chat Completions does not surface reasoning tokens); spec/llm-provider/conformance/ (eleven new fixtures033-043covering the Anthropic mapping rows, the cross-§-sectionThinkingBlocksemantics, and the §8.1 strip behavior). - Related: 0006 (llm-provider core + §8.1 OpenAI mapping — established the wire-format-mapping pattern this proposal extends to a second provider), 0015 (multimodal images — defined the §3.1 content-block shape this proposal expands), 0016 (structured output — defined the §6
response_schemasurface this proposal maps to Anthropic's nativeoutput_config.formatfield), 0019 (multi-provider wire-format extension — established the §8 catalog framing), 0025 (tool_choice — defined the §5tool_choiceparameter this proposal maps to Anthropic's distinct shape), 0026 (§8.X subsection template — defined the canonical five-subsection structure §8.2 follows), 0032 (RuntimeConfig surface refinements — declared field set this proposal maps to Anthropic's wire body) - Supersedes:
Summary¶
Add a normative wire-format mapping for the Anthropic Messages API
(POST /v1/messages) as §8.2 of llm-provider, following the §8.X
subsection template (Request / Response / Error / Concurrency /
Structured output) locked in by proposal 0026.
Anthropic's wire shape diverges from OpenAI Chat Completions enough
that §8.1's mapping cannot be reused: system is a top-level
request field rather than a message role, tool calls and tool
results are content blocks inside assistant and user messages
respectively (rather than a separate tool_calls field and tool
role), tool_choice has a different shape, max_tokens is
required, the tool definition uses input_schema rather than
function.parameters, and provider-emitted reasoning content
(thinking blocks) is round-trip-load-bearing for multi-turn
correctness on Anthropic's extended-thinking models.
The mapping spec'd here covers the conversational request/response surface with parity to §8.1 plus three Anthropic-specific sub-subsections:
- §8.2.1.1 — Content-block wire mapping. Maps spec
TextBlock/ImageBlock/ToolCallto Anthropic content blocks; introduces wire-level handling for the specThinkingBlockandRedactedThinkingBlocktypes this proposal adds to §3.1. - §8.2.1.2 —
toolrole bidirectional translation. Spec's §3toolrole messages have no Anthropic-side counterpart; this sub-subsection specifies the bidirectional translation between spectoolmessages and Anthropic user messages carryingtool_resultcontent blocks. - §8.2.5 — Structured output. Anthropic provides native
schema-constrained decoding via the top-level
output_config.formatfield (GA on current models); this sub-subsection specifies the native path (mirroring §8.1.5) plus fallbacks (tool-call coercion, then prompt-augmentation) for models predating native support.
Two §3.1 ContentBlock additions, one §8.1 strip-on-send rule, no changes to §3 role set, §4 Tool definition, §5 Provider interface, §6 Response shape, §7 error categories, §9 Determinism, §10 Out of scope, or any other §-section beyond the touchpoints listed above.
Motivation¶
The Anthropic Messages API is the second major provider protocol adopted across the OA implementation surface (after OpenAI Chat Completions, which §8.1 already covers). The spec's §8 catalog framing (per proposal 0019) intentionally placed the OpenAI mapping as the first concrete entry and reserved §8.2, §8.3, etc. for provider-native shapes that diverge enough to require their own mappings.
Anthropic diverges from OpenAI Chat Completions across the request, response, and structured-output surfaces in ways that make §8.1's mapping rules incorrect to apply:
- System extraction. OpenAI puts
systemas amessagesarray entry withrole: "system"; Anthropic puts it as a top-level request field. Reusing §8.1's mapping would put specsystemmessages into Anthropic'smessagesarray as regular entries — Anthropic rejects this with HTTP 400. - Tool call placement. OpenAI puts tool calls in
assistant.tool_calls(sibling tocontent); Anthropic puts them astool_usecontent blocks insideassistant.content. Reusing §8.1's mapping would emit tool calls in a field Anthropic doesn't recognize. - Tool result placement. OpenAI uses a
toolrole for tool results; Anthropic has notoolrole — tool results go astool_resultcontent blocks inside user messages, paired with thetool_use_idthat generated them. - Tool definition shape. OpenAI wraps in
{type: "function", function: {name, description, parameters}}; Anthropic uses{name, description, input_schema}directly. Field names differ; nesting differs. tool_choiceshape. OpenAI's specific-tool form is{type: "function", function: {name}}; Anthropic's is{type: "tool", name}(flatter; differenttypediscriminator values:"auto" | "any" | "tool" | "none"rather than"auto" | "required" | "none" | {function: {name}}).max_tokensrequirement. OpenAI permitsmax_tokensto be omitted (with provider default); Anthropic requires it on every request.- Structured output. Both have native schema-constrained
decoding, but via different fields: OpenAI's
response_format(§8.1.5) vs Anthropic's top-leveloutput_config.format. The §8.2 mapping targets Anthropic's native field; older Claude models without it fall back to tool-call coercion or prompt-augmentation. - Provider-emitted reasoning content. Anthropic's
extended-thinking models (Claude 3.7+ on the line; Claude 4
family) emit
thinkingandredacted_thinkingcontent blocks alongside text in assistant responses. The blocks carry a cryptographicsignaturethat Anthropic verifies on round-trip; preserving thinking blocks across multi-turn conversation flows (especially tool-use-with-thinking) is load-bearing for the model to maintain reasoning continuity. Without first-class spec representation, callers wanting multi-turn flows would have to dig intoResponse.raw, extract thinking blocks, and manually reconstruct the next request — bypassing the §3/§6 abstraction entirely for the reasoning-model use case.
Codifying these mappings in spec is required for the same
cross-language behavioral consistency that §8.1 provides for the
OpenAI-compatible case: sibling language implementations
targeting Anthropic must agree on the wire shape, the
bidirectional tool role translation, the structured-output
strategy, and the thinking-block round-trip semantics. Without
the §8.2 spec, language sibs would drift in subtle wire details
and break the cross-language promise.
Detailed design¶
§3.1 — Content-block expansion¶
§3.1's intro (currently "v1 defines two block types: text and image") updates to enumerate four block types: text, image, thinking, and redacted_thinking.
Two new sub-subsections added to §3.1 between the existing §3.1.3 Image source and the Mixing blocks section, which renumbers from §3.1.4 to §3.1.6:
§3.1.4 Thinking block¶
A thinking block is a record:
| Field | Required | Description |
|---|---|---|
type |
yes | The literal string "thinking". |
text |
yes | The reasoning content the provider emitted. A non-empty string. |
signature |
yes | An opaque provider-issued token used by the provider to verify the block on round-trip. Implementations MUST pass the value through unchanged; spec callers MUST NOT construct, modify, or fabricate the field. |
Thinking blocks represent provider-emitted reasoning content.
They MAY appear in assistant message content sequences (alongside
TextBlock and ToolCall entries — see §3 message shape — and
content-block sequences mixing them are valid). They MUST NOT
appear in user, system, or tool message content. Implementations
MUST surface thinking blocks emitted by a provider on the
Response.message.content block list (per §6) and MUST preserve
them verbatim when the same assistant message is sent back to
that provider in a subsequent complete() call.
Provider mappings that do not surface reasoning content (e.g., the §8.1 OpenAI mapping) MUST strip thinking blocks from outbound assistant messages per §8.1's strip-on-send rule (see §8.1 extension below), but MUST NOT emit thinking blocks on inbound responses. Wire-level behavior for each provider is specified in its §8.X mapping.
§3.1.5 Redacted thinking block¶
A redacted thinking block is a record:
| Field | Required | Description |
|---|---|---|
type |
yes | The literal string "redacted_thinking". |
data |
yes | An opaque provider-issued blob preserving the structural slot for reasoning content that the provider has redacted from caller view. Implementations MUST pass the value through unchanged. |
The redacted variant covers cases where a provider's policy
withholds the reasoning text from the caller while preserving the
structural slot — necessary so that subsequent turns of the
conversation can be round-tripped without breaking the provider's
reasoning continuity. Same scope rules as ThinkingBlock:
assistant-message-content-only, round-trip-load-bearing.
§3.1.6 Mixing blocks — clarifying update (renumbered from §3.1.4)¶
The existing prose covers text and image blocks. Update adds one sentence: "Assistant messages MAY also contain thinking and redacted-thinking blocks (per §3.1.4 and §3.1.5) when the provider mapping surfaces them; thinking blocks SHOULD precede text blocks in an assistant message's content sequence, matching the order providers emit them. Implementations MUST preserve the emitted order on round-trip."
§6 — Response.message.content clarifying note¶
§6's Response.message.content already accepts a content-block
list (per §3's content-block model). No structural change is
needed; this proposal adds a one-sentence clarifying note that
the content list MAY include ThinkingBlock and
RedactedThinkingBlock entries for provider mappings that surface
reasoning content.
§8.1 — Strip-on-send rule for non-reasoning mappings¶
§8.1.1 Request mapping extends with one new paragraph specifying
the strip-on-send rule for ThinkingBlock and
RedactedThinkingBlock entries: when an assistant message in the
spec request contains thinking blocks (e.g., because the caller
is passing back conversation history that originated from a
different §8.X-mapped provider), the §8.1 mapping MUST strip
those blocks before emitting the OpenAI wire request. OpenAI Chat
Completions does not surface reasoning tokens, has no wire-level
representation for thinking blocks, and rejects requests
containing block types it does not recognize. Stripping preserves
the spec's content-block superset across cross-provider
conversation round-trips: callers MAY route a conversation that
originated from Anthropic (and therefore carries thinking blocks
in its assistant history) through an OpenAI-compatible provider
without manual block filtering. Implementations MUST document
this strip-on-send behavior. Strip is deterministic; no error is
raised. The §8.1 mapping MUST NOT emit thinking blocks on inbound
responses (OpenAI Chat Completions doesn't produce them).
§8.2 — Anthropic Messages mapping (new)¶
The Anthropic Messages API (POST /v1/messages) is the
provider-native protocol for Anthropic's Claude model family.
§8.2.1 Request mapping¶
The §3 message list maps onto Anthropic's request body as follows:
System extraction. Any §3 messages with role: "system" are
removed from the spec message list and their content is
concatenated (text-only) into Anthropic's top-level system
request-body field. Concatenation joins the per-message contents
with \n\n (two newlines) when more than one system message is
present, preserving the order they appeared in the spec list.
The messages array sent to Anthropic contains only
role: "user" and role: "assistant" entries.
Image content blocks MUST NOT appear in system-role messages
(per §3.1 — image blocks are user-message-only). Implementations
MUST reject at pre-send validation (provider_invalid_request)
any system message containing non-text content.
Message body shape. Each remaining spec message maps to one Anthropic message:
| Spec role | Anthropic role | Notes |
|---|---|---|
user |
user |
When content is a string, maps directly. When content is a content-block sequence (§3.1), maps to Anthropic's content-array form per §8.2.1.1. |
assistant (no tool calls, no thinking) |
assistant |
content becomes Anthropic's content (string or block array). |
assistant (with tool calls and/or thinking) |
assistant |
Tool calls become tool_use content blocks in Anthropic's content array; thinking blocks pass through unchanged. See §8.2.1.1. |
tool |
(no direct Anthropic role) | Maps via §8.2.1.2 bidirectional translation to an Anthropic user message containing one tool_result content block. |
Tool definitions. A §4 Tool {name, description, parameters}
maps to an Anthropic tools entry as:
Note Anthropic uses input_schema, not parameters; the spec
parameters field's JSON Schema content passes through verbatim
under the renamed key.
tool_choice mapping. The §5 tool_choice parameter maps to
Anthropic's tool_choice request-body field per the table:
Spec tool_choice |
Anthropic wire body |
|---|---|
None / absent |
(field omitted from request body) |
"auto" |
{"type": "auto"} |
"required" |
{"type": "any"} (Anthropic's name for "require some tool") |
"none" |
{"type": "none"} |
{type: "tool", name: X} |
{"type": "tool", "name": X} |
The "required" → "any" rename is the load-bearing translation:
the spec's name is the cross-vendor stable form (matches OpenAI's
wire name), and Anthropic uses "any" for the same semantic.
Implementations of the Anthropic mapping MUST perform this
rename.
RuntimeConfig field mapping. The §6 RuntimeConfig declared
fields map to the Anthropic request body as follows:
temperature,top_p,seed,stop_sequences— map directly (same name on the Anthropic request body;stop_sequencesmatches Anthropic's wire-key convention exactly, no rename needed).max_tokens— maps directly. Anthropic requires this field on every request; ifRuntimeConfig.max_tokensisNoneor absent at the call site, implementations MUST reject at pre-send validation (provider_invalid_request) with a message identifyingmax_tokensas required by the Anthropic mapping. The spec MUST NOT default to a magic value; the caller decides.frequency_penalty,presence_penalty— Anthropic does NOT support these fields. If supplied (non-None), implementations MUST raiseprovider_invalid_requestat pre-send validation with a message identifying the unsupported field. Quiet drop is forbidden; the caller's intent is preserved by surfacing the mismatch.
The bound model identifier becomes Anthropic's model field.
Undeclared RuntimeConfig fields appear at the Anthropic
request-body root, as siblings to temperature, model, etc.,
per §6's extras-pass-through contract. The §8.2 mapping does NOT
validate, rename, or transform undeclared keys.
§8.2.1.1 Content-block wire mapping¶
This sub-subsection covers two wire-encoding paths:
- Spec content blocks (per §3.1 —
TextBlock,ImageBlock,ThinkingBlock,RedactedThinkingBlock) appearing in spec messagecontentfields map directly to Anthropic wire content entries per the table below. - Spec ToolCall records appearing in the assistant message's
top-level
tool_callsfield (per §3) are NOT §3 content blocks — the wire mapping extracts them and serializes them as Anthropictool_usewire entries in the assistant's content array. Reverse on receive: Anthropictool_usewire entries in the assistant's content array are parsed back into specToolCallrecords on the response'sResponse.message.tool_callsfield (per §6).
| Spec source | Anthropic wire entry |
|---|---|
TextBlock { text } (content block) |
{ "type": "text", "text": <text> } |
ImageBlock with source: url { url } (content block; user-only per §3.1) |
{ "type": "image", "source": { "type": "url", "url": <url> } }. The detail hint, when set on the spec block, is dropped — Anthropic does not honor detail. |
ImageBlock { media_type, source: inline { base64_data } } (content block; user-only per §3.1) |
{ "type": "image", "source": { "type": "base64", "media_type": <media_type>, "data": <base64_data> } }. The detail hint, when set, is dropped. |
ToolCall { id, name, arguments } from assistant tool_calls field (NOT a content block; extracted at wire) |
{ "type": "tool_use", "id": <id>, "name": <name>, "input": <arguments> }. The spec stores arguments as a deserialized mapping; Anthropic's wire format accepts an object directly under input. No JSON-string serialization step needed (unlike §8.1.1). |
ThinkingBlock { text, signature } (content block; assistant-only) |
{ "type": "thinking", "thinking": <text>, "signature": <signature> }. The signature passes through verbatim in both directions. |
RedactedThinkingBlock { data } (content block; assistant-only) |
{ "type": "redacted_thinking", "data": <data> }. The data blob passes through verbatim in both directions. |
Empty content blocks (text with empty text, image with both
sources absent) are spec-invalid and MUST be rejected at
pre-send validation per §3 / provider_invalid_request.
Anthropic and OpenAI use different content-block wire shapes across all block types; the spec's content-block model abstracts over the divergence so application code does not need per-provider conditionals.
§8.2.1.2 tool role bidirectional translation¶
Spec messages with role: "tool" (§3) do not map to any
Anthropic message role directly. Instead, the §8.2 mapping
translates bidirectionally:
Spec → Anthropic (on send): Each consecutive run of spec
tool messages between two non-tool messages collapses into a
single Anthropic user message whose content is an array of
tool_result blocks — one per original spec tool message,
preserving order:
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": <tool_call_id_from_spec_tool_msg>,
"content": <content_from_spec_tool_msg> }
/* one per consecutive spec tool message */
]
}
This collapse is required because Anthropic forbids consecutive
messages of the same role in messages, and a user message
already follows the model's prior tool calls; sending each tool
result as its own user message would violate Anthropic's role
alternation rule.
Anthropic → Spec (on receive): When parsing Anthropic
conversation history (e.g., when a caller passes back
Response.raw.content or constructs a follow-up complete()
call from a prior response), each tool_result content block
inside a user message maps back to one spec tool message with
tool_call_id from tool_use_id and content from the block's
content. The user message's other content blocks (if any —
e.g., a user text block preceding the tool results) form a
separate spec user message. The reverse translation preserves
the original tool-call/tool-result pairing without information
loss.
The translation is lossless and bidirectional: a spec → Anthropic → spec round-trip preserves message-role and tool-call relationships.
§8.2.2 Response mapping¶
A successful Anthropic response maps onto a §6 Response as
follows:
message— built from the response'srole: "assistant"plus thecontentarray. Each content-array entry maps back to its spec content block per §8.2.1.1's table (text → TextBlock, tool_use → ToolCall, thinking → ThinkingBlock, redacted_thinking → RedactedThinkingBlock). The order of blocks in the response is preserved on the specMessage.contentlist.tool_calls— extracted from the assistant's content array (anytool_useblock becomes a specToolCallon the message; the §6Response.message.tool_callsmirrors this for compatibility with §8.1's flatter shape).tool_useblocks remain in thecontentarray as well — they appear in both places so that callers using either access pattern see the same tool calls.finish_reason— derived from Anthropic'sstop_reason:
Anthropic stop_reason |
Spec finish_reason |
|---|---|
"end_turn" |
"stop" |
"max_tokens" |
"length" |
"stop_sequence" |
"stop" (the matched sequence is preserved in Response.raw.stop_sequence) |
"tool_use" |
"tool_calls" |
"pause_turn" |
"stop" (a long-running turn the provider paused; the caller MAY continue by passing the response back. The pause is preserved in Response.raw.stop_reason for callers implementing the continue protocol) |
"refusal" |
"content_filter" (the refusal category, when present, is preserved in Response.raw.stop_details) |
| (unknown) | "error" |
usage— built from Anthropic'susagefield:usage.prompt_tokens←input_tokens,usage.completion_tokens←output_tokens,usage.total_tokens← sum of the prompt and completion token counts the mapping reports (computed when both are present, otherwiseNoneper §6's usage rules). Cache-token note: Anthropic'sinput_tokenscounts only the non-cached input; cached input is reported separately incache_creation_input_tokensandcache_read_input_tokens. Anthropic's own total-input accounting is thereforeinput_tokens + cache_creation_input_tokens + cache_read_input_tokens. The specusage.prompt_tokensmaps frominput_tokensalone (the non-cached portion); the cache-related subfields appear inResponse.raw.usageunchanged and are NOT promoted to the specusagerecord (cache primitives are out of scope per Out of scope below). Implementations whose cost accounting needs the full input total read the cache subfields fromResponse.raw.raw— the parsed JSON body of the Anthropic response, verbatim. Implementations MUST NOT redact, rewrite, or omit fields. Anthropic-specific extensions (e.g., the responseid, themodelactually used, cache token counts) surface here unchanged.
§8.2.3 Error mapping¶
| Anthropic condition | Spec category |
|---|---|
HTTP 401 authentication_error |
provider_authentication |
HTTP 402 billing_error |
provider_authentication (account-level access failure — the spec groups it with auth; the specific billing_error type appears in Response.raw) |
HTTP 403 permission_error |
provider_authentication (the spec category groups auth + permission failures; the specific Anthropic type appears in Response.raw if needed) |
HTTP 404 not_found_error (model-not-found body) |
provider_invalid_model |
HTTP 413 request_too_large |
provider_invalid_request (the request exceeds Anthropic's size limit) |
HTTP 429 rate_limit_error |
provider_rate_limit |
HTTP 500 api_error |
provider_unavailable |
HTTP 504 timeout_error |
provider_unavailable |
HTTP 529 overloaded_error |
provider_unavailable |
| HTTP 5xx (other), connection error, client timeout | provider_unavailable |
| HTTP 400 with body indicating the bound model rejected a content block (image/media-type/unsupported source variant rejection) | provider_unsupported_content_block |
HTTP 400 invalid_request_error (other malformed-request causes) |
provider_invalid_request |
| Successful HTTP response that fails to parse into §6 shape | provider_invalid_response |
The error envelope is {"type": "error", "error": {"type":
<error_type>, "message": <string>}, "request_id": <string>}.
Anthropic's per-error error.type field and the request_id
surface in Response.raw for callers needing finer-grained
handling.
§8.2.4 Concurrency¶
Matches §8.1.4. Anthropic's hosted API supports concurrent
requests; implementations MUST NOT add a serialization layer.
Concurrent complete() calls go to the wire concurrently. Client-side
rate-limit needs use the pipeline-utilities rate limiter or
middleware, not this layer.
§8.2.5 Structured output¶
The Anthropic Messages API provides native structured output
(generally available on current Claude models) via the top-level
output_config.format request field. The spec defines a native
path (mirroring §8.1.5) plus a fallback for models predating
native support.
Native: output_config.format. When complete() is called
with a response_schema, the §8.2 mapping sets:
The type: "json_schema" discriminator is required; the supplied
response_schema JSON Schema passes through under schema. The
GA path requires no beta header. Anthropic's constrained decoding
guarantees the generated output conforms to the schema. The
structured JSON is returned as the assistant message's text
content; the §8.2 mapping parses it into Response.parsed and
validates against response_schema per §6. On validation failure
raise structured_output_invalid (§7).
Two non-conformance cases are inherent to the provider and
NOT validation bugs: a stop_reason: "refusal" (safety refusal —
the output may not match the schema because the refusal takes
precedence) and a stop_reason: "max_tokens" (truncation — the
output is incomplete). In both cases the §8.2 mapping surfaces the
non-conforming content and the stop_reason; whether to raise
structured_output_invalid or surface the degraded response
follows §6 / §7's existing rules for the corresponding
finish_reason (content_filter / length). Implementations
MUST NOT silently coerce these into a schema-conforming shape.
When complete() is called without a response_schema, the
request MUST NOT include output_config; the free-form wire
shape is preserved.
(Anthropic also offers a complementary strict-tool-use feature —
strict: true on individual tool definitions — that guarantees
tool-call argument conformance rather than response shape.
That is a tool-parameter feature, not the §6 response_schema
surface; it is reachable via the tool-definition extras path and
is not part of this structured-output mapping.)
§8.2.5.1 Fallback for models without native structured output¶
Claude models predating native output_config.format support
fall back to one of the pre-native patterns. Two are available;
implementations SHOULD prefer tool-call coercion (stronger
conformance) and MUST document which path a given call uses.
Tool-call coercion (preferred fallback). When the caller's
tools list is empty or absent, construct a synthetic tool whose
input_schema is the response_schema, add it to the tools
array, and set tool_choice to {"type": "tool", "name":
<synthetic name>}. The response's tool_use.input for the
synthetic tool becomes Response.parsed. This was Anthropic's
documented best practice before native structured output and
remains the strongest fallback. Unavailable when the caller
already supplies tools (the synthetic tool would override the
caller's tool intent).
Prompt-augmentation (last-resort fallback). Per §8.1.5.1:
append a schema directive to the system field (or message list),
issue the request unmodified otherwise, parse and validate the
text response against response_schema, raise
structured_output_invalid on failure. The caller's original
messages MUST be left unchanged.
Implementations MUST document which path is selected for a given call and SHOULD expose a way for callers to inspect or override the choice.
Spec-text changes (summary)¶
Eight edits to spec/llm-provider/spec.md:
- §3 assistant per-role constraint — relax
contentfrom "non-empty string" to "non-empty string OR a non-empty ordered sequence of content blocks per §3.1." The content-block sequence MAY containTextBlockandThinkingBlock/RedactedThinkingBlockentries;ImageBlockremains user-only (per §3.1's existing constraints).ToolCallstays in the existing top-leveltool_callsfield on assistant messages — it is NOT a §3 content-block type; §8.X mappings that wire-encode tool calls as content blocks (e.g., §8.2'stool_usewire blocks) extract fromtool_callsat the wire boundary. - §3.1 intro — update "v1 defines two block types" to enumerate four block types.
- §3.1.4 Thinking block — new sub-subsection (existing Mixing blocks §3.1.4 renumbers to §3.1.6).
- §3.1.5 Redacted thinking block — new sub-subsection.
- §3.1.6 Mixing blocks (renumbered from §3.1.4) — clarifying update for thinking block ordering.
- §6 Response message — one-sentence clarifying note that
Response.message.contentMAY includeThinkingBlockandRedactedThinkingBlockentries. - §8.1.1 — strip-on-send rule for
ThinkingBlock/RedactedThinkingBlockin assistant messages (since OpenAI doesn't accept those wire shapes). - §8.2 (new) — full Anthropic Messages mapping per §8.X template.
No changes to §3 role set or to ToolCall placement (still a
top-level tool_calls field on assistant messages), §4 Tool
definition, §5 Provider interface, §6 Response (beyond the
clarifying note), §7 error categories, §9 Determinism, §10
Out of scope, or any other §-section.
Conformance fixtures¶
Eleven new fixture pairs under spec/llm-provider/conformance/:
| Fixture | Asserts |
|---|---|
033-anthropic-basic-message-round-trip |
Simple user→assistant text round-trip; system extraction; messages array contains only user/assistant entries; top-level system carries the concatenated system content; response parses into spec Response with text-only message.content. |
034-anthropic-tool-call-flow |
Multi-turn flow: user → assistant with tool_use blocks → spec tool messages → Anthropic user message with tool_result blocks (per §8.2.1.2 bidirectional translation) → assistant final response. Verifies tool-call wire shape, tool role collapse on send, tool_result parsing on receive. |
035-anthropic-image-content-blocks |
Both URL and base64 inline image variants; verifies detail hint is stripped (Anthropic doesn't honor it). |
036-anthropic-tool-choice-modes |
All five tool_choice shapes (None/absent, "auto", "required" → "any" rename, "none", specific tool by name). |
037-anthropic-runtime-config-mapping |
RuntimeConfig declared fields map to the Anthropic body. temperature, top_p, seed, stop_sequences pass through verbatim; max_tokens passes through; frequency_penalty and presence_penalty raise provider_invalid_request if supplied. |
038-anthropic-max-tokens-required |
Pre-send validation rejects calls without max_tokens (raises provider_invalid_request); calls with max_tokens set proceed. |
039-anthropic-error-mapping |
HTTP status + Anthropic error.type → spec §7 category table per §8.2.3. |
040-anthropic-structured-output-native |
Native path: caller supplies response_schema; the wire request carries output_config.format = {type: "json_schema", schema} with the schema verbatim; the response's text content is parsed into Response.parsed. |
041-anthropic-structured-output-fallback |
Tool-call-coercion fallback (for models without native support): the wire request introduces the synthetic tool with tool_choice set to it; the response's tool_use.input becomes Response.parsed. |
042-anthropic-thinking-block-round-trip |
Multi-turn flow with extended-thinking response: first call returns assistant content containing thinking + text blocks; second call passes the assistant message back verbatim; the wire request preserves thinking blocks unchanged with signatures intact; the second response also parses correctly. |
043-openai-strips-thinking-blocks |
Cross-mapping interop: a spec assistant message containing thinking blocks is routed through the §8.1 OpenAI mapping; the wire request to OpenAI strips the thinking blocks and contains only text; no error is raised; the response parses normally. |
Eleven fixtures total. The eleventh covers the §8.1 strip-on-send rule from the §3.1 expansion.
Versioning¶
MINOR bump v0.28.0. Additive normative changes:
- New §8.2 wire-format mapping (additive — does not change §8.1 or any other §-section's behavior).
- Two new content-block types in §3.1 (additive — existing content-block consumers continue to work since the new types appear only in assistant messages from §8.2-mapped providers, and §8.1 strips them on send).
- One new strip-on-send rule in §8.1 (additive — affects outbound wire requests only when an assistant message contains thinking blocks, which prior to this proposal could not occur).
No breaking changes. Existing callers using only §3 text/image blocks and §8.1 routing continue to work unchanged. Implementations of §8.1 that don't handle the new strip rule would still work for prior callers (no thinking blocks ever appeared) but would fail conformance fixture 043 once it lands.
Backwards compatibility¶
- For callers using §8.1 with text/image only: no change. No thinking blocks appear; no strip step runs.
- For callers using §8.1 with cross-provider conversation history: new strip-on-send rule activates when an assistant message containing thinking blocks (from a prior Anthropic call) is routed through OpenAI. Wire stays valid; thinking context is lost on the OpenAI side (as it would be regardless, since OpenAI doesn't surface reasoning).
- For callers using §8.2 (new): full mapping behavior per this proposal; no prior contract to preserve.
- For §3.1 ContentBlock consumers: the spec's content-block
list extends with two new types. Implementations that switch
exhaustively on
typewill need to add cases or fall through to default-handling; the spec recommends pass-through-or-strip patterns for non-reasoning-aware code paths.
Out of scope¶
- Streaming. Anthropic's SSE streaming surface has different event types from OpenAI's; spec-level streaming awaits a cross-mapping streaming proposal.
- Message Batches API (
POST /v1/messages/batches). Anthropic's asynchronous batch surface is operationally different from the synchronouscomplete()shape this mapping covers: callers submit N requests, the provider processes asynchronously, and callers fetch results when ready. Spec-level batch support is a future capability — likely a new set ofProvideroperations (submit_batch,get_batch_status,get_batch_results) with per-vendor wire mappings at §8.X.6 (or equivalent), composing with the graph-engine suspension primitive (currently Draft per proposal 0021) for the wait-for-result flow. OpenAI's Batch API and Gemini's equivalent surfaces fall under the same future capability. - Token Counting API (
POST /v1/messages/count_tokens). Anthropic's pre-flight token-counting endpoint computes the input token count for a message payload without sending the payload to a model. Useful for cost estimation, pre-send rejection when payloads exceed context limits, and client-side rate-limit budgeting. Spec-level support is a future capability — likely a newProvider.count_tokens(messages, tools=None) -> intoperation with per-vendor wire mappings (Anthropic native endpoint, Gemini nativemodels.countTokensmethod, OpenAI client-side tokenizer fallback since OpenAI has no native endpoint). - Models API (
GET /v1/models). Anthropic's model-listing endpoint enumerates available models with metadata (context limits, capabilities, pricing tier). Spec-level support is a future capability — likely a newProvider.list_models() -> list[ModelInfo]operation with per-vendor wire mappings, or a staticModelInforegistry the spec defines and implementations populate from vendor APIs. Lower priority than the batch and token-counting capabilities since most callers know their bound model. - Prompt caching. Anthropic's
cache_control: {type: "ephemeral"}content-block primitive is omitted from spec normative coverage; it remains user-extensible via §6's extras-pass-through (callers may attachcache_controlto wire-level content blocks via implementation-specific extension paths). Cross-vendor caching is a future spec topic once patterns settle. - Document blocks. Anthropic's
documentcontent blocks (PDF inputs) would require a §3.1 ContentBlock expansion beyond text/image/thinking/redacted_thinking; deferred to a future proposal. The Files API upload lifecycle (upload →file_id→ reference in a message) rides with document-block support. - Server-side tools. Anthropic-executed tools (
web_search,code_execution,web_fetch,tool_search) declared in the request (e.g.,{"type": "web_search_20260209", "name": ...}) and producing distinct response content blocks (server_tool_use,web_search_tool_result,web_fetch_tool_result,code_execution_tool_result). This is a different request/response shape from §8.2's client-tooltool_use/tool_result(where the caller executes) and is not covered here. A future proposal would add server-tool declaration and result-block mapping — cross-vendor, since OpenAI built-in tools and Gemini grounding are analogous. - Request-side reasoning controls. §8.2 maps thinking blocks
on the response; it does not define a first-class surface for
requesting / budgeting extended thinking (Anthropic's
thinkingrequest param). Callers reach it today via §6's extras-pass-through (an undeclaredthinkingbody field). A first-class cross-vendor reasoning-control surface (pairing with Gemini'sthinkingConfig) is a future topic; this proposal covers reasoning content, not reasoning control. - Request-header passthrough (incl. beta features). §6's
extras-pass-through covers request-body fields; it does not
cover request headers. Anthropic pre-GA features gated behind
the
anthropic-betaheader are therefore not reachable through the declared surface. A general request-header passthrough mechanism (cross-vendor — any versioned/beta-header API) is a future topic. - Agentic tool-execution loop. The Anthropic SDK's tool-runner (auto-execute a tool call, feed the result back, iterate) is intentionally NOT a provider concern: per llm-provider §1 the provider is stateless and does not loop on tool calls. An agent is a graph whose conditional edge loops back to the LLM node — the loop lives at the graph layer, not in §8.2. This is a design boundary, not an omission.
- Cross-vendor reasoning abstraction. This proposal
introduces
ThinkingBlockandRedactedThinkingBlockat the spec level but only maps them in §8.2 (Anthropic). If a future provider's reasoning content fits the same block shape, its §8.X mapping can adopt the existing blocks; if not, that proposal introduces sibling block types or expands the spec abstraction. This proposal does NOT pre-commit to a cross-vendor reasoning shape beyond Anthropic.
Open questions¶
None at draft time. The six design decisions surfaced during
scoping (structured-output approach, max_tokens requirement,
multiple system messages, extended thinking treatment, prompt
caching scope, tool role round-trip) are resolved above. The
§3.1 expansion is scoped to two new block types (thinking +
redacted thinking) with explicit message-role and round-trip
constraints. The §8.1 strip-on-send rule provides cross-mapping
interop without requiring caller changes.