translate

package
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 31, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package translate converts Anthropic Messages API requests/responses to and from OpenAI ChatCompletions shape. Pure: no I/O, no upstream calls. Adapter-specific quirks live in internal/adapter/<provider>.

Index

Constants

View Source
const (
	ErrTypeInvalidRequest = "invalid_request_error"
	ErrTypeAuthentication = "authentication_error"
	ErrTypePermission     = "permission_error"
	ErrTypeRateLimit      = "rate_limit_error"
	ErrTypeAPI            = "api_error"
	ErrTypeOverloaded     = "overloaded_error"
)

Anthropic error "type" taxonomy — the canonical wire strings for the inner `error.type` of the Anthropic error envelope. Single source of truth, shared by the server (client-side errors) and the OpenAI-dialect translator's upstream-error re-wrap (FromUpstreamError). The identity translator never uses them: it forwards the native-Anthropic error body verbatim.

Variables

View Source
var (
	ErrUpstreamMalformed = errors.New("upstream returned malformed JSON")
	ErrBackTranslation   = errors.New("back-translation failed")
)

Translator error categories. The server maps these to HTTP status: ErrBackTranslation (a structurally-valid upstream response shim can't convert) is a 500; a malformed upstream body is a 502 (gateway problem).

Functions

func AnthropicErrorJSON added in v0.3.1

func AnthropicErrorJSON(errType, message string) []byte

AnthropicErrorJSON marshals an Anthropic-shaped error envelope. errType is one of the ErrType* constants; message is human-readable. Marshalling two strings cannot fail in practice — the fallback is defensive only, never reached.

Types

type AnthropicBlock

type AnthropicBlock struct {
	Type string `json:"type"`

	// type: "text"
	Text string `json:"text,omitempty"`

	// type: "image"
	Source *AnthropicImageSource `json:"source,omitempty"`

	// type: "tool_use"
	ID    string          `json:"id,omitempty"`
	Name  string          `json:"name,omitempty"`
	Input json.RawMessage `json:"input,omitempty"`

	// type: "tool_result"
	ToolUseID string `json:"tool_use_id,omitempty"`
	// Content on tool_result is either a string or a content[] of text/image
	// blocks. We keep RawMessage for the boundary.
	ToolContent json.RawMessage `json:"content,omitempty"`
	IsError     bool            `json:"is_error,omitempty"`

	// type: "thinking" (Stage 2.6c). Thinking carries the model's reasoning
	// text; Signature is constant "shim-passthrough-v1" — shim doesn't
	// verify on roundtrip and DeepSeek discards it. See README "Errors and
	// debugging" for the no-verification posture rationale.
	Thinking  string `json:"thinking,omitempty"`
	Signature string `json:"signature,omitempty"`
}

AnthropicBlock is a tagged union of content block types: text, image, tool_use, tool_result, and thinking. Stage 2.6c lifts the assistant-side thinking 501; user-side thinking is still rejected because the Anthropic spec only produces thinking on assistant turns.

type AnthropicImageSource

type AnthropicImageSource struct {
	Type      string `json:"type"`       // "base64" | "url"
	MediaType string `json:"media_type"` // e.g. "image/png"
	Data      string `json:"data,omitempty"`
	URL       string `json:"url,omitempty"`
}

AnthropicImageSource carries either base64 data with a media_type or a URL. Anthropic supports both; OpenAI takes a single URL form (data: for base64).

type AnthropicMessage

type AnthropicMessage struct {
	Role    string          `json:"role"`
	Content json.RawMessage `json:"content"`
}

AnthropicMessage holds a role and content. Content is either a string or an array of typed blocks — we keep RawMessage for the boundary and parse in helpers.

type AnthropicRequest

type AnthropicRequest struct {
	Model         string             `json:"model"`
	Messages      []AnthropicMessage `json:"messages"`
	System        json.RawMessage    `json:"system,omitempty"` // string | content[]
	MaxTokens     int                `json:"max_tokens"`
	Stream        bool               `json:"stream,omitempty"`
	Temperature   *float64           `json:"temperature,omitempty"`
	TopP          *float64           `json:"top_p,omitempty"`
	StopSequences []string           `json:"stop_sequences,omitempty"`
	Tools         []AnthropicTool    `json:"tools,omitempty"`
	ToolChoice    json.RawMessage    `json:"tool_choice,omitempty"`
	Thinking      *ThinkingConfig    `json:"thinking,omitempty"`
}

AnthropicRequest mirrors the documented Anthropic Messages request body. Fields shim doesn't act on are omitted (e.g. metadata, service_tier, top_k); json.Unmarshal DROPS them, so re-marshalling this struct loses them. The translating path only needs the modelled fields; the identity (passthrough) translator forwards the original request bytes verbatim to avoid the drop.

type AnthropicResponse

type AnthropicResponse struct {
	ID           string           `json:"id"`
	Type         string           `json:"type"` // always "message"
	Role         string           `json:"role"` // always "assistant"
	Model        string           `json:"model"`
	Content      []AnthropicBlock `json:"content"`
	StopReason   string           `json:"stop_reason"`
	StopSequence *string          `json:"stop_sequence,omitempty"`
	Usage        AnthropicUsage   `json:"usage"`
}

AnthropicResponse is what we send back to Claude Code.

func OpenAIToAnthropic

func OpenAIToAnthropic(resp *OpenAIResponse, originalModel string) (*AnthropicResponse, error)

OpenAIToAnthropic converts an OpenAI ChatCompletions response back to Anthropic Messages shape. Only choice[0] is consumed (Stage 0; n=1).

originalModel is the model name the client sent (passed through into the response so Claude Code sees what it asked for).

type AnthropicTool

type AnthropicTool struct {
	Name        string          `json:"name"`
	Description string          `json:"description,omitempty"`
	InputSchema json.RawMessage `json:"input_schema"`
}

AnthropicTool is the tool declaration shape on the request side. Schema stays a RawMessage so we don't validate JSON Schema here.

type AnthropicUsage

type AnthropicUsage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}

AnthropicUsage mirrors Anthropic's usage shape.

type OpenAIChoice

type OpenAIChoice struct {
	Index        int           `json:"index"`
	Message      OpenAIMessage `json:"message"`
	FinishReason string        `json:"finish_reason"`
}

OpenAIChoice is one completion option. We only consume index 0.

type OpenAIContentPart

type OpenAIContentPart struct {
	Type     string          `json:"type"` // "text" | "image_url"
	Text     string          `json:"text,omitempty"`
	ImageURL *OpenAIImageURL `json:"image_url,omitempty"`
}

OpenAIContentPart represents a single chunk within a multi-modal user message: text or image_url.

type OpenAIImageURL

type OpenAIImageURL struct {
	URL string `json:"url"`
}

OpenAIImageURL accepts http(s) URLs or data: URLs for inline base64.

type OpenAIMessage

type OpenAIMessage struct {
	Role             string           `json:"role"`
	Content          json.RawMessage  `json:"content,omitempty"`
	ToolCalls        []OpenAIToolCall `json:"tool_calls,omitempty"`
	ToolCallID       string           `json:"tool_call_id,omitempty"`
	ReasoningContent string           `json:"reasoning_content,omitempty"`
}

OpenAIMessage covers system, user, assistant, and tool roles. Content is string for simple cases or an array of parts when mixed text+image. ReasoningContent (Stage 2.6c) carries DeepSeek's thinking-mode reasoning text on assistant messages — emitted by upstream in responses, optionally echoed back in continuation requests so the upstream's "reasoning_content required on tool continuations" contract holds.

type OpenAIRequest

type OpenAIRequest struct {
	Model       string          `json:"model"`
	Messages    []OpenAIMessage `json:"messages"`
	MaxTokens   int             `json:"max_tokens,omitempty"`
	Temperature *float64        `json:"temperature,omitempty"`
	TopP        *float64        `json:"top_p,omitempty"`
	Stop        []string        `json:"stop,omitempty"`
	Tools       []OpenAITool    `json:"tools,omitempty"`
	ToolChoice  json.RawMessage `json:"tool_choice,omitempty"`
	Stream      bool            `json:"stream,omitempty"`
	Thinking    *ThinkingConfig `json:"thinking,omitempty"`
}

OpenAIRequest is the body we send to {adapter.BaseURL}/chat/completions.

func AnthropicToOpenAI

func AnthropicToOpenAI(req *AnthropicRequest) (*OpenAIRequest, error)

AnthropicToOpenAI converts an Anthropic Messages request to an OpenAI ChatCompletions request. Tools-related fields are delegated to tools.go (step 5) but the wiring lives here.

Model name mapping is delegated to the Adapter — translate stays pure.

type OpenAIResponse

type OpenAIResponse struct {
	ID      string         `json:"id"`
	Model   string         `json:"model"`
	Choices []OpenAIChoice `json:"choices"`
	Usage   OpenAIUsage    `json:"usage"`
}

OpenAIResponse is the upstream response we receive.

type OpenAITool

type OpenAITool struct {
	Type     string             `json:"type"` // always "function"
	Function OpenAIToolFunction `json:"function"`
}

OpenAITool is the function-wrapped tool declaration.

type OpenAIToolCall

type OpenAIToolCall struct {
	ID       string             `json:"id"`
	Type     string             `json:"type"` // always "function"
	Function OpenAIToolCallBody `json:"function"`
}

OpenAIToolCall is one tool invocation on the assistant side.

type OpenAIToolCallBody

type OpenAIToolCallBody struct {
	Name      string `json:"name"`
	Arguments string `json:"arguments"` // JSON-encoded string per spec
}

OpenAIToolCallBody is the name + serialised JSON arguments.

type OpenAIToolFunction

type OpenAIToolFunction struct {
	Name        string          `json:"name"`
	Description string          `json:"description,omitempty"`
	Parameters  json.RawMessage `json:"parameters"`
}

OpenAIToolFunction inner declaration.

type OpenAIUsage

type OpenAIUsage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
}

OpenAIUsage carries prompt/completion token counts.

type SSEEvent

type SSEEvent struct {
	Name string
	Data any
}

SSEEvent is one Anthropic-shaped SSE event, ready to wire-format. Marshal as `event: <Name>\ndata: <JSON(Data)>\n\n`.

func ToAnthropicSSE

func ToAnthropicSSE(resp *OpenAIResponse, originalModel string) ([]SSEEvent, error)

ToAnthropicSSE converts a complete OpenAI response into an ordered sequence of Anthropic SSE events. Stage 0 emits the sequence in a single burst (buffer-then-restream) so callers see the canonical event order even though no real per-token streaming happens. Tool-use blocks emit one content_block_start + a single input_json_delta carrying the full arguments + content_block_stop.

type ThinkingConfig added in v0.5.1

type ThinkingConfig struct {
	Type string `json:"type"`
}

ThinkingConfig is the extended-thinking control param. The {type} shape is identical on both wire formats — Anthropic's request thinking field and the OpenAI/DeepSeek-dialect thinking field — so one type serves both. Only Type ("enabled"|"disabled") is inspected; other fields (budget_tokens, display, …) are unmarshalled and dropped.

type Translator added in v0.3.0

type Translator interface {
	// ToUpstream builds the upstream request body. req is the parsed request;
	// raw is the original inbound bytes (the identity translator forwards them
	// verbatim, so client fields shim doesn't model survive). mappedModel is
	// the upstream model name (from Adapter.MapModel). stopCapped reports how
	// many stop sequences the dialect dropped, so the server emits the
	// loud-fail metric/log (thesis-2) while the translator stays pure. The
	// implementation owns the upstream `stream` flag for its dialect.
	ToUpstream(req *AnthropicRequest, raw []byte, mappedModel string) (body []byte, stopCapped int, err error)

	// FromUpstream converts a buffered (non-streaming) upstream response body
	// into the Anthropic-shaped response BYTES the client should receive, plus
	// normalized usage so the caller records the token delta without knowing
	// the dialect. Returning bytes (not a struct) lets the identity translator
	// forward the upstream body verbatim — lossless passthrough, no field-drop
	// from re-encoding through shim's response struct.
	FromUpstream(body []byte, originalModel string) (respBody []byte, usage AnthropicUsage, err error)

	// StreamChunks returns a pull iterator over wire-ready Anthropic SSE event
	// bytes (`event: <name>\ndata: <json>\n\n`). The caller writes + flushes
	// each chunk. usage is populated by the time next reports ok=false. The
	// implementation owns reading resp.Body; the caller has already gated on a
	// 2xx upstream status.
	StreamChunks(resp *http.Response, originalModel string) (next func() ([]byte, bool, error), usage *AnthropicUsage, err error)

	// FromUpstreamError renders a NON-2xx upstream response for the client,
	// returning the client-facing status and body. It is the error-path analog
	// of FromUpstream: the dialect — not the handler — owns how an upstream
	// failure surfaces. The OpenAI dialect re-classifies the status and emits a
	// fresh Anthropic-shaped error envelope (the OpenAI body is OpenAI-shaped,
	// may carry prompt content, and must not leak to an Anthropic client);
	// identity forwards the upstream status + body verbatim (the upstream IS
	// native Anthropic — its error is already correctly shaped, so re-wrapping
	// would only lose fidelity). The server owns logging, measurement, and the
	// write; this method owns only the dialect render.
	FromUpstreamError(upstreamStatus int, upstreamBody []byte) (clientStatus int, clientBody []byte)
}

Translator converts between the Anthropic Messages wire format and one upstream transport dialect. shim supports two dialects: OpenAI ChatCompletions (the openaichat preset family — see AnthropicOpenAI) and identity (anthropic-passthrough — defined in the passthrough adapter).

Implementations are pure for request/response (no client I/O, no config, no state). StreamChunks reads the upstream response body (input) but never touches the client ResponseWriter — the server handler owns the writer and the flush loop. This keeps dialect knowledge out of the handler entirely.

func AnthropicOpenAI added in v0.3.0

func AnthropicOpenAI() Translator

AnthropicOpenAI returns the Translator for OpenAI-ChatCompletions-dialect upstreams. It wraps the pure AnthropicToOpenAI / OpenAIToAnthropic / ToAnthropicSSE functions. Streaming is buffer-then-restream: the upstream is driven non-streaming and the canonical SSE sequence is synthesized from the complete response (true per-token streaming for translating providers is out of v1.0 scope).

func Identity added in v0.3.0

func Identity() Translator

Identity returns the passthrough Translator for a native-Anthropic upstream: the request is forwarded verbatim and the response body is returned unchanged, with no dialect conversion. Used by the anthropic-passthrough adapter. Forwarding bytes verbatim is the point — shim must not drop Anthropic response fields it doesn't model.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL