Add the Anthropic passthrough renderer and client (A10/A13) — step 7a #20

Merged
dongar merged 1 commits from anthropic-renderer into main 2026-09-08 00:01:30 +00:00
Collaborator

First of three PRs for step 7. This is the route delegations will use.

Why a second renderer rather than reusing the normalized one. The vendors disagree structurally, which is exactly A13's argument for normalising in the renderer instead of on the wire:

OpenAI-compatible Anthropic Messages
system prompt a message with role system a top-level system field
tool call tool_calls array on the message a tool_use content block
tool result a message with role tool a tool_result block in a user message
tool schema function.parameters input_schema

A middlebox flattening those would be lossy precisely where a long-running delegation lives.

  • render_anthropic.py builds vendor-native /v1/messages bodies from the canonical log, coalescing adjacent same-role blocks so a tool_result lands directly after its tool_use — which the API requires.
  • Tool manifests stay declared once in the OpenAI shape and are converted per vendor, so adding a tool does not mean editing two places and letting them drift.
  • AnthropicPassthroughClient implements the same LLMClient protocol, so a caller picks a route, not a different kind of client — which is what lets the delegation harness use either without knowing.
  • Dangling tool calls get a placeholder result here too; this API rejects them more strictly than the OpenAI shape does.

Verified live, not assumed. Three integration tests: a plain turn, a full tool-call round trip where the tool_result is fed back and accepted (this is what actually proves the placement is right), and a check that cache_read_input_tokens is present in the passthrough's usage — A5 explicitly asks that prompt caching be verified to survive the passthrough, and a proxy that flattened the request would turn a cost optimisation into a silent per-call expense.

One deliberate gap, flagged rather than hidden: vendor-opaque reasoning blocks (thinking, redacted_thinking) are dropped. A10 wants them stored, tagged with the producing model, and never replayed outside the session that produced them, and the canonical schema has no event for that yet. Nothing requests extended thinking, so none arrive today — but it needs a canonical representation before it does, and 7b is the natural place since replay-within-session is a delegation concern.

11 new unit tests, 3 live. 198 unit + 13 integration green locally.

🤖 Generated with Claude Code

First of three PRs for step 7. This is the route delegations will use. **Why a second renderer rather than reusing the normalized one.** The vendors disagree *structurally*, which is exactly A13's argument for normalising in the renderer instead of on the wire: | | OpenAI-compatible | Anthropic Messages | |---|---|---| | system prompt | a message with role `system` | a top-level `system` field | | tool call | `tool_calls` array on the message | a `tool_use` content block | | tool result | a message with role `tool` | a `tool_result` block in a **user** message | | tool schema | `function.parameters` | `input_schema` | A middlebox flattening those would be lossy precisely where a long-running delegation lives. - **`render_anthropic.py`** builds vendor-native `/v1/messages` bodies from the canonical log, coalescing adjacent same-role blocks so a `tool_result` lands directly after its `tool_use` — which the API requires. - **Tool manifests stay declared once** in the OpenAI shape and are converted per vendor, so adding a tool does not mean editing two places and letting them drift. - **`AnthropicPassthroughClient` implements the same `LLMClient` protocol**, so a caller picks a *route*, not a different kind of client — which is what lets the delegation harness use either without knowing. - Dangling tool calls get a placeholder result here too; this API rejects them more strictly than the OpenAI shape does. **Verified live, not assumed.** Three integration tests: a plain turn, a full tool-call round trip where the `tool_result` is fed back and accepted (this is what actually proves the placement is right), and a check that `cache_read_input_tokens` is present in the passthrough's usage — A5 explicitly asks that prompt caching be verified to survive the passthrough, and a proxy that flattened the request would turn a cost optimisation into a silent per-call expense. **One deliberate gap, flagged rather than hidden:** vendor-opaque reasoning blocks (`thinking`, `redacted_thinking`) are dropped. A10 wants them stored, tagged with the producing model, and never replayed outside the session that produced them, and the canonical schema has no event for that yet. Nothing requests extended thinking, so none arrive today — but it needs a canonical representation before it does, and 7b is the natural place since replay-within-session is a delegation concern. 11 new unit tests, 3 live. 198 unit + 13 integration green locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dev-agent added 1 commit 2026-09-07 23:13:23 +00:00
Add the Anthropic passthrough renderer and client (A10/A13) — 7a
CI / checks (push) Successful in 44s
CI / checks (pull_request) Successful in 43s
f4a94281ef
The second renderer adapter, and the concrete case for A13's decision
to normalise in the renderer rather than on the wire. The two vendors
disagree structurally, not cosmetically: the system prompt is a
top-level field rather than a message, a tool call is a tool_use
content block rather than an array, a tool result is a block inside a
user message rather than a "tool" role, and tool schemas use
input_schema rather than parameters. A middlebox flattening those
would be lossy exactly where a long-running delegation lives.

- render_anthropic.py builds vendor-native /v1/messages bodies from
  the canonical log, coalescing adjacent same-role blocks so a
  tool_result lands directly after its tool_use
- Tool manifests are declared once in OpenAI shape and converted per
  vendor, so adding a tool does not mean editing two places
- AnthropicPassthroughClient implements the same LLMClient protocol,
  so callers pick a route rather than a different kind of client, and
  maps provider tool ids to canonical ones at the boundary
- Dangling tool calls get a placeholder result here too; this API
  rejects them more strictly than the OpenAI shape
- Vendor-opaque reasoning blocks are dropped for now with the reason
  recorded: A10 wants them stored and never replayed across sessions,
  and the canonical schema has no event for that yet

Live tests prove the body is accepted as sent, that a tool_result
turn round-trips, and — as A5 asks — that prompt cache accounting
survives the passthrough rather than being flattened by the proxy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dongar approved these changes 2026-09-08 00:01:26 +00:00
dongar merged commit 1c8e7e35fc into main 2026-09-08 00:01:30 +00:00
dongar deleted branch anthropic-renderer 2026-09-08 00:01:30 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dev/nisse#20