Documentation
¶
Index ¶
- func DecodeCountTokensRequest(req *http.Request) (codec.DecodedRequest, error)
- func DecodeResponse(body []byte) (*inference.Response, error)
- func EncodeRequest(req inference.Request, stream bool) ([]byte, error)
- func MatchCountTokensRequest(req *http.Request) bool
- func WriteCountTokensResponse(w http.ResponseWriter, inputTokens int) error
- type Codec
- func (Codec) DecodeEvent(event []byte) ([]content.Chunk, error)
- func (Codec) DecodeRequest(req *http.Request) (codec.DecodedRequest, error)
- func (Codec) DecodeResponse(body []byte) (*inference.Response, error)
- func (Codec) DecodeStream(resp *http.Response) (*stream.StreamReader[content.Chunk], error)
- func (Codec) EncodeRequest(req inference.Request, mode codec.RequestMode) (codec.EncodedRequest, error)
- func (Codec) MatchRequest(req *http.Request) bool
- func (Codec) OpenStream(w http.ResponseWriter) (codec.StreamEncoder, error)
- func (Codec) WriteError(w http.ResponseWriter, err error)
- func (Codec) WriteResponse(w http.ResponseWriter, resp *inference.Response) error
- type ConversationCollisionError
- type DuplicateKeyError
- type EmptyTextBlockError
- type ForeignThinkingSignatureError
- type InvalidToolNameError
- type InvalidToolSchemaError
- type InvalidToolUseIDError
- type SamplingRangeError
- type ServerDecodeError
- type StreamAPIError
- type StreamDecodeError
- type StreamEventDecodeError
- type StreamTerminatedError
- type ThinkingBudgetError
- type UndeclaredThinkingDialectError
- type UnsupportedAudioError
- type UnsupportedBlockError
- type UnsupportedChunkError
- type UnsupportedConversationError
- type UnsupportedDocumentError
- type UnsupportedEffortError
- type UnsupportedImageMediaTypeError
- type UnsupportedRefusalError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DecodeCountTokensRequest ¶
func DecodeCountTokensRequest(req *http.Request) (codec.DecodedRequest, error)
DecodeCountTokensRequest decodes a POST /v1/messages/count_tokens request using the same semantic message/tool/system/image/thinking decoding as DecodeRequest. The count_tokens wire body has the same shape as a Messages request minus max_tokens/stream (both are optional on decode already), so it shares decodeMessagesBody. Streaming is always false: count_tokens has no streaming mode. As with decodeMessagesRequest, Request.Model stays unresolved.
func DecodeResponse ¶
DecodeResponse parses a non-streaming Anthropic Messages API response body into a provider-neutral *inference.Response. An `error`-type envelope (a 200 body carrying {"type":"error",...}) is surfaced as a *failure.APIError. An empty content array is a valid response (e.g. a refusal or a pure stop), not an error.
func EncodeRequest ¶
EncodeRequest converts a provider-neutral inference.Request into an Anthropic `POST /v1/messages` JSON body. stream=true adds "stream":true to the body. Request.System becomes the top-level `system` field; any SystemMessage in the thread is folded into it (Anthropic has no in-thread system role).
func MatchCountTokensRequest ¶
MatchCountTokensRequest reports whether req is a POST /v1/messages/count_tokens request. It is not part of codec.ServerCodec: the count_tokens endpoint is a separate, narrower auxiliary the gateway wires up on its own route, composing this decode helper with its own target resolution and a contextcount.ContextCounter.
func WriteCountTokensResponse ¶
func WriteCountTokensResponse(w http.ResponseWriter, inputTokens int) error
WriteCountTokensResponse writes Anthropic's count_tokens response shape given an already-computed token count. It does not compute the count itself: the gateway resolves the target model and calls a contextcount.ContextCounter, then calls this helper with the result.
Types ¶
type Codec ¶
type Codec struct{}
Codec is the Anthropic Messages API wire dialect expressed as an codec.Codec (and, via DecodeStream, an codec.StreamingCodec). It is stateless (an empty struct with value-receiver methods), so one value is safely shared across goroutines: the transport owns HTTP mechanics, the Codec owns the JSON body, per-event semantics, and SSE stream decoding. The methods delegate to package-level free functions so the method surface and the free surface cannot diverge.
func (Codec) DecodeEvent ¶
DecodeEvent decodes one already-de-framed SSE event payload into the chunk(s) it yields. It is stateless, and tolerant of every uninteresting or unknown but VALID event (message_start, content_block_stop, message_delta, message_stop, ping, unrecognized future types, …), which return (nil, nil) — a skip, not an error. Malformed JSON is the one intolerant case: it yields a *StreamEventDecodeError, because a truncated frame is indistinguishable from a dropped one and skipping it would let a lossy stream report success. Cross-event assembly (concatenating a tool call's start + input_json_delta fragments into a ToolUseBlock) happens downstream in the stream accumulator, not here.
func (Codec) DecodeRequest ¶
DecodeRequest decodes a matched POST /v1/messages request into a codec.DecodedRequest, delegating to the free decodeMessagesRequest.
func (Codec) DecodeResponse ¶
DecodeResponse parses a non-streaming Anthropic Messages response body, delegating to the free DecodeResponse.
func (Codec) DecodeStream ¶
DecodeStream frames a successful Anthropic Messages streaming response with wire/sse and maps each frame through the codec's per-event decode logic. The message_stop marker authorizes the terminal result but yields no chunk; the body's natural EOF ends the transport stream. Because that EOF is indistinguishable from a dropped connection, a body that reaches it without message_stop fails with a *StreamDecodeError rather than reporting a clean, truncated success. It owns resp.Body: the returned reader's Close closes it.
func (Codec) EncodeRequest ¶
func (Codec) EncodeRequest(req inference.Request, mode codec.RequestMode) (codec.EncodedRequest, error)
EncodeRequest builds the Anthropic Messages request: a JSON body reader plus the application/json content type as an EncodedRequest. RequestModeStream sets "stream":true in the body, every other mode omits it.
func (Codec) MatchRequest ¶
MatchRequest reports whether req is a POST /v1/messages request.
func (Codec) OpenStream ¶
func (Codec) OpenStream(w http.ResponseWriter) (codec.StreamEncoder, error)
OpenStream begins the native Anthropic Messages streaming response and returns its request-scoped StreamEncoder, delegating to the free openMessagesStream.
func (Codec) WriteError ¶
func (Codec) WriteError(w http.ResponseWriter, err error)
WriteError encodes err as the native Anthropic error envelope, delegating to the free writeMessageError.
func (Codec) WriteResponse ¶
WriteResponse encodes a complete inference.Response as the native Anthropic Messages API non-streaming response, delegating to the free writeMessageResponse.
type ConversationCollisionError ¶
type ConversationCollisionError struct {
Reason string
}
ConversationCollisionError reports adjacent neutral turns that cannot be combined without violating Anthropic's tool-result ordering rules.
func (*ConversationCollisionError) Error ¶
func (e *ConversationCollisionError) Error() string
type DuplicateKeyError ¶
type DuplicateKeyError struct {
Key string
}
DuplicateKeyError reports a request body with a duplicate JSON object member name. encoding/json silently takes the last occurrence; this codec rejects the request instead so a client cannot smuggle a semantically different value past a naive review of the first occurrence.
func (*DuplicateKeyError) Error ¶
func (e *DuplicateKeyError) Error() string
type EmptyTextBlockError ¶
type EmptyTextBlockError struct{}
EmptyTextBlockError is returned by the encoder when a text content block carries no text. Anthropic's RequestTextBlock declares `text` required with minLength 1, so an empty text block is not a representable wire shape: it would encode to the invalid `{"type":"text"}` and draw an HTTP 400. The codec refuses it here instead, fail-secure like UnsupportedBlockError, so the defect surfaces at the call site rather than as an opaque provider rejection.
func (*EmptyTextBlockError) Error ¶
func (e *EmptyTextBlockError) Error() string
type ForeignThinkingSignatureError ¶
type ForeignThinkingSignatureError struct {
// Format is the label carried by the signature, or "" when it carries none.
Format string
}
ForeignThinkingSignatureError reports a reasoning signature this dialect cannot prove it minted, either because another dialect's label is attached (Format names it) or because no label is attached at all (Format is empty).
It is a hard error rather than a degrade, which is the one design decision in this type worth stating. Probed against api.anthropic.com with claude-haiku-4-5 on 2026-08-13: a verbatim signature is accepted; a signature with eight characters changed draws `messages.N.content.0: Invalid "signature" in "thinking" block` (HTTP 400); and an EMPTY signature draws that same 400. So both available degrades lose. Forwarding a foreign signature sends a request that is certain to be rejected, and stripping it sends an unsigned thinking block that is equally certain to be rejected — while destroying the only copy of the continuation state on the way. Failing here costs the same turn and names the cause.
The realistic source is not an attacker. Bedrock Converse and the Messages API are two endpoints for the same Claude models; their thinking blocks are structurally identical and their signatures are not interchangeable, so a session moved between them carries a block nothing else can tell apart.
func (*ForeignThinkingSignatureError) Error ¶
func (e *ForeignThinkingSignatureError) Error() string
type InvalidToolNameError ¶
InvalidToolNameError is returned by the encoder when a tool name cannot satisfy Anthropic's ^[a-zA-Z0-9_-]{1,128}$. Tool servers, MCP ones in particular, routinely publish names containing "." or "/".
func (*InvalidToolNameError) Error ¶
func (e *InvalidToolNameError) Error() string
type InvalidToolSchemaError ¶
InvalidToolSchemaError is returned by the encoder when a tool's input schema is not a JSON object, which Anthropic's InputSchema requires.
func (*InvalidToolSchemaError) Error ¶
func (e *InvalidToolSchemaError) Error() string
type InvalidToolUseIDError ¶
InvalidToolUseIDError is returned by the encoder when a tool_use id or a tool_result tool_use_id cannot satisfy Anthropic's ^[a-zA-Z0-9_-]+$. Identifiers are minted by whichever provider issued the call, so a conversation replayed from a dialect with a wider class (Bedrock Converse permits "." and ":") can carry one Anthropic rejects. An empty id is the worse case: it is omitempty on the wire, so it does not travel as "" — the required property simply disappears.
func (*InvalidToolUseIDError) Error ¶
func (e *InvalidToolUseIDError) Error() string
type SamplingRangeError ¶
SamplingRangeError is returned by the encoder when temperature or top_p falls outside the [0, 1] interval Anthropic declares. The shared model.Sampling vocabulary is wider (an OpenAI-shaped temperature runs to 2), so switching a session onto an Anthropic model can carry a value the API refuses.
func (*SamplingRangeError) Error ¶
func (e *SamplingRangeError) Error() string
type ServerDecodeError ¶
ServerDecodeError reports a native Messages request body this codec cannot decode into the provider-neutral vocabulary: malformed shape, a missing required field, or a recognized-but-unsupported feature (e.g. a tool_choice variant or thinking mode the neutral Request cannot represent). Reason is a short machine-checkable diagnostic code; Detail elaborates for logs/messages.
func (*ServerDecodeError) Error ¶
func (e *ServerDecodeError) Error() string
type StreamAPIError ¶
StreamAPIError reports an Anthropic error event received after a streaming request crossed the successful HTTP-status boundary. It retains only the provider's structured error type and message, never the raw response frame.
func (*StreamAPIError) Error ¶
func (e *StreamAPIError) Error() string
type StreamDecodeError ¶
StreamDecodeError reports a Messages stream that is framed and parseable but structurally wrong — currently only a body that reaches EOF without the message_stop event the MessageStreamEvent union ends with, which means the answer was truncated in flight. It never includes the raw provider payload in its diagnostic. Named and shaped after the equivalent type in codec/bedrockconverse and codec/geminiapi. It lives here rather than in errors.go because the gate it serves is the only thing that raises it.
func (*StreamDecodeError) Error ¶
func (e *StreamDecodeError) Error() string
func (*StreamDecodeError) Unwrap ¶
func (e *StreamDecodeError) Unwrap() error
type StreamEventDecodeError ¶
type StreamEventDecodeError struct{ Err error }
StreamEventDecodeError reports malformed JSON inside an otherwise successfully framed Anthropic Messages stream. A truncated or corrupt frame is indistinguishable from a dropped one, so it is a terminal decode failure rather than a skip: swallowing it would let a stream that lost content still report an authoritative clean success. Unknown-but-VALID event types remain tolerant skips — only unparseable bytes reach here.
func (*StreamEventDecodeError) Error ¶
func (e *StreamEventDecodeError) Error() string
func (*StreamEventDecodeError) Unwrap ¶
func (e *StreamEventDecodeError) Unwrap() error
type StreamTerminatedError ¶
type StreamTerminatedError struct{}
StreamTerminatedError is returned by StreamEncoder.WriteChunk, Finish, or Fail once the stream has already been terminated by a prior Finish or Fail call, per the single-termination-ownership rule in codec.StreamEncoder.
func (*StreamTerminatedError) Error ¶
func (e *StreamTerminatedError) Error() string
type ThinkingBudgetError ¶
ThinkingBudgetError is returned by the encoder when the budget dialect has no legal budget_tokens available for the request's max_tokens.
Two constraints bound the field and only one of them is in the schema. ThinkingConfigEnabled declares budget_tokens with minimum 1024; Anthropic documents separately that it must be less than max_tokens, which is a cross-field rule no JSON Schema keyword in the derived document expresses — the gate was measured accepting budget_tokens 99999 against max_tokens 1024. Both together make max_tokens 1025 the smallest cap that admits any legal budget, so a smaller cap is refused here, where the diagnostic can name the field, rather than at the provider.
func (*ThinkingBudgetError) Error ¶
func (e *ThinkingBudgetError) Error() string
type UndeclaredThinkingDialectError ¶
type UndeclaredThinkingDialectError struct {
Model string
}
UndeclaredThinkingDialectError is returned by the encoder when a request asks for reasoning from a model advertised as thinking-capable whose Caps.ThinkingDialect the catalogue never declared.
Anthropic serves two on-modes concurrently and rejects the wrong one with an HTTP 400 rather than degrading: measured on 2026-08-13, claude-haiku-4-5 answers `{"type":"adaptive"}` with "adaptive thinking is not supported on this model", and claude-sonnet-5 answers `{"type":"enabled","budget_tokens":N}` with "\"thinking.type.enabled\" is not supported for this model. Use \"thinking.type.adaptive\" and \"output_config.effort\"". Nothing in the request document distinguishes them, so with no declared dialect the encoder has a coin flip between two bodies, one of which is provably rejected.
It therefore fails closed and names the model, because that is the piece of information the fix needs: a provider 400 says the request was wrong, this says which catalogue row is incomplete. The provider's WithThinking escape hatch remains available for a caller who knows better than the catalogue.
func (*UndeclaredThinkingDialectError) Error ¶
func (e *UndeclaredThinkingDialectError) Error() string
type UnsupportedAudioError ¶
type UnsupportedAudioError struct {
MediaType string
}
UnsupportedAudioError is returned by the encoder for an audio content block.
This is a hard limitation of the format, not a gap in this codec: the Anthropic Messages API document declares no audio content block in any request or response shape, and the substring "audio" does not occur anywhere in it. An audio block therefore has no wire form to encode toward, and there is nothing to route it to.
It gets a typed error of its own rather than the generic UnsupportedBlockError because this path is reachable in ordinary operation: an MCP tool returning an audio result produces a content.AudioBlock that the harness clones and persists, so the failure surfaces on every subsequent turn of that session and the message has to say why.
func (*UnsupportedAudioError) Error ¶
func (e *UnsupportedAudioError) Error() string
type UnsupportedBlockError ¶
type UnsupportedBlockError struct {
Block string
}
UnsupportedBlockError is returned by the encoder when a content block has a concrete type the Anthropic Messages API dialect does not model (e.g. audio or document blocks). Block holds the Go type name for diagnosis. Callers may errors.As to detect an unencodable block rather than string-matching.
func (*UnsupportedBlockError) Error ¶
func (e *UnsupportedBlockError) Error() string
type UnsupportedChunkError ¶
type UnsupportedChunkError struct {
Chunk string
}
UnsupportedChunkError is returned when a content.Chunk has a concrete type this dialect's stream encoder does not model. content.Chunk is a sealed interface, so this only guards against future variants added to the vocabulary.
func (*UnsupportedChunkError) Error ¶
func (e *UnsupportedChunkError) Error() string
type UnsupportedConversationError ¶
type UnsupportedConversationError struct {
Conversation string
}
UnsupportedConversationError is returned by the encoder when a conversation turn has a concrete type outside the closed content.Conversation union the dialect maps (user / assistant / tool-result / system). Conversation holds the Go type name for diagnosis.
func (*UnsupportedConversationError) Error ¶
func (e *UnsupportedConversationError) Error() string
type UnsupportedDocumentError ¶
type UnsupportedDocumentError struct {
Reason string
}
UnsupportedDocumentError is returned by the encoder when a document block has no legal RequestDocumentBlock form. Reason names the violated constraint.
The neutral content.DocumentBlock is wider than Anthropic's source union in both directions that matter. Base64PDFSource.media_type is const "application/pdf" and PlainTextSource.media_type is const "text/plain", so a DOCX payload or a markdown body has no representable source at all — and re-labelling either one to satisfy the const would forward a document the caller never described. RequestDocumentBlock.title is additionally capped at 500 characters, which a file name can exceed.
func (*UnsupportedDocumentError) Error ¶
func (e *UnsupportedDocumentError) Error() string
type UnsupportedEffortError ¶ added in v0.12.0
type UnsupportedEffortError struct {
Effort string
}
UnsupportedEffortError reports a neutral reasoning effort for which the Anthropic Messages wire contract has no symbolic effort member.
func (*UnsupportedEffortError) Error ¶ added in v0.12.0
func (e *UnsupportedEffortError) Error() string
type UnsupportedImageMediaTypeError ¶
type UnsupportedImageMediaTypeError struct {
MediaType string
}
UnsupportedImageMediaTypeError is returned by the encoder when an inline image block carries a media type outside Anthropic's Base64ImageSource enum (image/jpeg, image/png, image/gif, image/webp). The shared content.MediaType vocabulary is wider — content.MediaTypeImageSVG has no Anthropic equivalent — so this is a representable Looprig block with no representable wire form, refused here for the same reason as EmptyTextBlockError.
func (*UnsupportedImageMediaTypeError) Error ¶
func (e *UnsupportedImageMediaTypeError) Error() string
type UnsupportedRefusalError ¶
type UnsupportedRefusalError struct{}
UnsupportedRefusalError is returned by the encoder for a content.RefusalBlock.
This is a hard limitation of the format, not a gap in this codec. Anthropic models a refusal as RESPONSE metadata — stop_reason "refusal" alongside a RefusalStopDetails object carrying a category and an explanation — and the Messages API request document declares no refusal content block in any position. A refusal therefore has no wire form to encode toward, and the alternatives are all worse than failing: sending it as `text` shows the model its own decline quoted back as something it said (the exact defect content.RefusalBlock exists to remove), and dropping it silently loses the fact that the turn was declined.
Like UnsupportedAudioError it gets a typed error of its own rather than the generic UnsupportedBlockError, and for the same reason: the path is reachable in ordinary operation. A refused turn is stored in session history, so the failure surfaces on every subsequent turn of that session and the message has to say why.
func (*UnsupportedRefusalError) Error ¶
func (e *UnsupportedRefusalError) Error() string