The Anthropic provider. Accepts OpenAI-shaped requests
(llmrouter.ChatRequest),
translates them to the Anthropic /v1/messages JSON
shape, then re-encodes Anthropic SSE events back as OpenAI-shaped
streaming chunks
(llmrouter.Chunk).
Source:
providers/anthropic/anthropic.go.
Issues a streaming /v1/messages request and returns a
*llmrouter.Stream that yields OpenAI-shaped chunks.
Source:
anthropic.go#L57.
Request translation
Performed by buildAnthropicBody. The output is the
Anthropic /v1/messages JSON body.
System lifting. Iterates req.Messages.
Any message with Role == "system" is removed from the
array and its
PlainText()
appended to a top-level system string (multiple
system messages are joined with "\n\n"). All other
messages are kept in order with Role and raw
Content bytes preserved.
max_tokens. Uses
req.MaxTokens directly when positive; otherwise
defaults to 4096 (defaultMaxTokens).
Anthropic requires this field.
stream. Always set to
true.
model. Passed through from
req.Model.
Raw passthrough for tuning knobs. When
req.Raw is non-empty, it is unmarshaled into a
map[string]json.RawMessage and the keys
temperature, top_p, top_k,
stop are lifted out. The stop key is
renamed to stop_sequences on the way through. (If
req.Raw is invalid JSON, the lift is silently
skipped.)
Typed fallback. When req.Raw is
empty, the typed fields are used instead:
req.Temperature (when non-nil),
req.TopP (when non-nil), req.Stop
(when non-empty, written as stop_sequences).
HTTP call
Method:POST
URL:<BaseURL>/messages
Headers:
Content-Type: application/json
Accept: text/event-stream
x-api-key: <api-key>
anthropic-version: 2023-06-01
HTTP client is p.cfg.HTTP() (lazy default, 120 s timeout).
Network errors are wrapped:
fmt.Errorf("anthropic: http: %w", err).
On HTTP ≥ 400, reads up to 8 KiB of
the body verbatim (no whitespace trim), closes the response, and
returns a *llmrouter.ErrUpstream with
Provider: "anthropic".
SSE pump
On success, spawns pump in a goroutine and returns a
*llmrouter.Stream immediately. The pump:
Generates a chat id once:
chatID := "chatcmpl-" + uuid.NewString(). This is
reused as Chunk.ID on every emitted chunk so
downstream consumers see a stable id across the whole stream.
Captures created := time.Now().Unix() once.
Reads the response body line by line via bufio.Scanner (initial buffer 64 KiB, max 1 MiB).
Tracks the current event: type and accumulates
data: lines. On a blank line, dispatches the
accumulated payload to handleEvent.
Before each scan iteration, checks ctx.Err() and
calls hooks.Finish(ctx.Err()) on cancel.
On scanner error other than io.EOF, calls
hooks.Finish(fmt.Errorf("anthropic: read stream: %w", err)).
On clean EOF without an explicit message_stop, calls hooks.Finish(nil).
The response body is closed via defer.
Event handling
Each event type is dispatched in handleEvent. Events
not listed below — ping, content_block_start,
content_block_stop, thinking_delta, and
anything unrecognised — are dropped silently. Malformed JSON on any
single event is tolerated and does not abort the stream.
Event
What gets emitted
Side effects
message_start
A role primer chunk:
Delta{Role: "assistant", Content: ""}, no
finish reason.
Captures message.usage.input_tokens into
state.inputTokens if > 0. Captures
message.model into state.model if
non-empty (overriding req.Model with the upstream's
canonical name).
content_block_delta
Only when delta.type == "text_delta" and
delta.text != "": a content chunk with
Delta{Content: delta.text}. Other delta
types (e.g. tool input deltas) are dropped.
None.
message_delta
When delta.stop_reason is non-empty: a final
chunk with empty Delta, the mapped
FinishReason, and the accumulated
Usage attached. Chunk.Raw is
re-marshaled after attaching usage so it reflects the
published chunk.
Captures usage.output_tokens into
state.outputTokens if > 0.
message_stop
Nothing.
Sets done = true; the pump calls hooks.Finish(nil) and returns.
All emitted chunks have:
Object: "chat.completion.chunk",
ID: chatID (the per-stream UUID),
Created: created,
Model: state.model,
Choices: [{Index: 0, Delta: ..., FinishReason: ...}],
and a pre-populated Raw field (the JSON marshaling of
the chunk itself).
Stop-reason mapping
Anthropic stop_reason → OpenAI
finish_reason:
Anthropic stop_reason
OpenAI finish_reason
end_turn
stop
stop_sequence
stop
max_tokens
length
tool_use
tool_calls
anything else
stop
Usage accounting
Anthropic reports input_tokens on
message_start and output_tokens on
message_delta. The provider accumulates both onto an
internal pumpState and emits them as
Usage
on the final chunk:
If both are zero (no message_start ever seen,
Anthropic-compatible relay that strips usage, etc.),
Chunk.Usage is left nil rather than reporting a fake
zero.