Last updated 2026-05-17

Package anthropic

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.

Import path:

import "github.com/elloloop/llmrouter/providers/anthropic"

Constants

const (
defaultBaseURL = "https://api.anthropic.com/v1"
anthropicVersion = "2023-06-01"
defaultMaxTokens = 4096
scannerBufferSize = 1024 * 1024 // 1 MiB
providerName = "anthropic"
)

All unexported. Source: anthropic.go#L24.

defaultBaseURL
Used when WithBaseURL is not supplied.
anthropicVersion
Sent verbatim as the anthropic-version request header.
defaultMaxTokens
Used when req.MaxTokens <= 0. Anthropic requires max_tokens, so this prevents 400 responses for callers who leave the field unset.
scannerBufferSize
1 MiB maximum SSE line length for bufio.Scanner.Buffer.

Provider

Talks to Anthropic /v1/messages. Source: anthropic.go#L33.

type Provider struct {
// unexported: cfg *llmrouter.Config
}

New

func New(opts ...llmrouter.Option) (*Provider, error)

Builds a Provider from llmrouter options. Source: anthropic.go#L38.

Behavior:

  • Calls llmrouter.NewConfig(opts...); surfaces the first option error.
  • Required: WithAPIKey. Returns fmt.Errorf("%w: anthropic requires an api key", llmrouter.ErrInvalidConfig) if missing.
  • Default BaseURL: https://api.anthropic.com/v1.

Example

p, err := anthropic.New(llmrouter.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
if err != nil {
log.Fatal(err)
}

Provider.Name

func (p *Provider) Name() string

Returns the literal string "anthropic". Source: anthropic.go#L53.

Provider.CompletionStream

func (p *Provider) CompletionStream(ctx context.Context, req llmrouter.ChatRequest) (*llmrouter.Stream, error)

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_turnstop
stop_sequencestop
max_tokenslength
tool_usetool_calls
anything elsestop

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:

Usage{
PromptTokens: state.inputTokens,
CompletionTokens: state.outputTokens,
TotalTokens: state.inputTokens + state.outputTokens,
}

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.

Example: minimal completion

p, err := anthropic.New(llmrouter.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
if err != nil {
log.Fatal(err)
}
stream, err := p.CompletionStream(context.Background(), llmrouter.ChatRequest{
Model: "claude-3-5-sonnet-20241022",
Messages: []llmrouter.Message{
llmrouter.TextMessage("system", "You are a concise assistant."),
llmrouter.TextMessage("user", "What is the Liskov substitution principle?"),
},
MaxTokens: 500,
})
if err != nil {
log.Fatal(err)
}
for chunk := range stream.Chunks() {
for _, c := range chunk.Choices {
fmt.Print(c.Delta.Content)
}
if chunk.Usage != nil {
fmt.Printf("\n[usage] in=%d out=%d total=%d\n",
chunk.Usage.PromptTokens, chunk.Usage.CompletionTokens, chunk.Usage.TotalTokens)
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}

Example: lifting tuning knobs via raw

raw := []byte(`{
"temperature": 0.3,
"top_p": 0.9,
"top_k": 40,
"stop": ["\n\nUser:"]
}`)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{
Model: "claude-3-5-sonnet-20241022",
Messages: []llmrouter.Message{llmrouter.TextMessage("user", "Tell me a joke.")},
Raw: raw,
})
// buildAnthropicBody extracts temperature, top_p, top_k, and renames
// stop -> stop_sequences in the outbound /v1/messages body.