translate

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 9 Imported by: 0

README

llm-translate

Zero-dependency Go library for bidirectional protocol translation between the Anthropic Messages API, OpenAI Chat Completions, and the OpenAI Responses API.

Features

  • Anthropic ↔ OpenAI Request & Response: Translation of messages, tool definitions, tool results, system prompts and vision content.
  • SSE Stream Translation: Real-time translation between OpenAI chunk deltas and Anthropic SSE events (message_start, content_block_start, content_block_delta, message_delta, message_stop).
  • OpenAI Responses ↔ Chat: Bidirectional translation for newer OpenAI Responses API backends to standard Chat completions.
  • Loss reports: every *WithReport conversion returns a machine-readable list of what the target surface could not represent.
  • Typed wire codecs: package wire decodes and re-encodes all three surfaces without losing unknown or vendor fields.
  • Pure Standard Library: Zero CGO, zero external dependencies.

Installation

go get github.com/xibodev/llm-translate

Quick Start

Convert Anthropic Messages Request to OpenAI
import "github.com/xibodev/llm-translate"

openaiMessages := translate.AnthropicMessagesToOpenAI(req.Messages, req.System)
tools := translate.AnthropicToolsToOpenAI(req.Tools)
Stream OpenAI Chunks as Anthropic SSE
translate.OpenAIStreamToAnthropicSSE(chunksIter, "claude-3-5-sonnet", func(sseEvent string) {
    w.Write([]byte(sseEvent))
    w.(http.Flusher).Flush()
})
Stream Anthropic SSE as OpenAI Chunks

AnthropicSSEToOpenAIChunks emits no usage and ends every stream with a finish chunk. AnthropicSSEToOpenAIChunksWithOptions emits the same chunks for a stream that completes, and returns a *StreamReport once lines is exhausted:

report := translate.AnthropicSSEToOpenAIChunksWithOptions(lines, model, emit,
    translate.StreamOptions{IncludeUsage: true})
var upstream *translate.StreamError
switch {
case report.Completed:
    // message_stop arrived and no error event did
case errors.As(report.Err, &upstream):
    // an error event: upstream.Type, upstream.Message
case errors.Is(report.Err, translate.ErrStreamTruncated):
    // the source ended before message_stop
}
  • report.Usage holds the input, cache creation, cache read and output tokens the source reported, also for a failed stream. PromptTokens() sums the three input counts, as Chat's prompt_tokens does.
  • IncludeUsage adds the chunk OpenAI sends for stream_options.include_usage: "choices": [] with prompt_tokens, completion_tokens, total_tokens and prompt_tokens_details.cached_tokens (the cache reads). Every other chunk then carries "usage": null. The chunk is sent only for a completed stream whose source reported usage.
  • A stream that is not Completed ends where its source did, without a finish or usage chunk, so a failure cannot pass for a complete answer.

Loss reports

Each *WithReport function returns the converted value with a Report. A Loss names the source value that changed with a dot-separated path into the source payload (numeric segments index arrays), a class (unsupported, dropped, approximated, renamed) and a severity:

  • material: the conversion changed or omitted meaning. RejectMaterialLoss turns these into an error.
  • advisory: the answer is intact. A product that depends on the value can still reject it by path, for example with a policy rule on **.reasoning.
result := translate.ChatToResponsesWithReport(model, messages, options, false)
if err := result.RejectMaterialLoss(); err != nil {
    return err
}
Vendor fields inside messages

Conversions carry a vendor field when the target can represent it, and report it at its exact path when they cannot:

Field Carried Otherwise reported as
Chat tool call extra_content.google.thought_signature (Gemini) never; no other surface has a place for it material, e.g. messages.3.tool_calls.0.extra_content.google.thought_signature
Chat tool call function.thought_signature or call-level thought_signature (other Gemini providers) never material, e.g. messages.3.tool_calls.0.function.thought_signature, messages.3.tool_calls.0.thought_signature
Chat message reasoning_details never advisory, e.g. messages.2.reasoning_details
Chat message reasoning_content never advisory, e.g. messages.2.reasoning_content
Chat content part cache_control to Anthropic, on text parts advisory, e.g. messages.0.content.1.cache_control
Anthropic block cache_control never; Chat has no breakpoints material (unsupported) by AnthropicRequestToOpenAIWithReport, e.g. system.0.cache_control

A dropped thought signature is material because Gemini needs it back on the next turn of a tool call. Each location that carries one is reported, and empty values carry nothing, so they are never reported. Response and stream conversions use the same field paths under choices.N.message and choices.N.delta. In a stream the index after tool_calls is the call's index field, which identifies it across chunks.

A dropped reasoning item or thinking block keeps its own path with a .reasoning suffix, such as output.2.reasoning, content.0.reasoning or messages.1.content.0.reasoning, so one rule matches reasoning on every surface. These losses are advisory, and they come in addition to whatever the conversion already reported for the item.

cache_control from Chat to Anthropic

OpenAIMessagesToAnthropic and its WithReport form turn user, assistant and tool content that has a cache_control text part into Anthropic text blocks. Text up to and including each marked part becomes one block carrying that cache_control, joined exactly as before, so the cached prefix is the one the Chat request marked. Content without cache_control converts as before.

The system prompt is returned as a string, so only OpenAIMessagesToAnthropicWithReport can carry system breakpoints: it sets AnthropicMessages.SystemBlocks, which should be sent as the request's system value instead of System. cache_control on a part the conversion drops, such as an image, is reported as advisory.

Package wire

github.com/xibodev/llm-translate/wire holds typed request, response and stream structs for the three surfaces:

Surface Request Response Stream
Chat Completions ChatRequest ChatResponse ChatChunk
Anthropic Messages AnthropicRequest AnthropicResponse AnthropicEvent
OpenAI Responses ResponsesRequest ResponsesResponse ResponsesEvent

Each has a Decode… function, such as wire.DecodeChatRequest, and encodes with json.Marshal. Stream types hold the JSON data of one SSE event, without the data: framing.

req, err := wire.DecodeChatRequest(body)
if err != nil {
    return err
}
for _, call := range req.Messages[len(req.Messages)-1].ToolCalls {
    log.Println(call.ID, call.ThoughtSignature())
}
out, err := json.Marshal(req) // equal to body as JSON
Round-trip guarantee

Decoding and then encoding reproduces the input as JSON: both parse to the same value, with numbers compared by their literal text. Member order and insignificant whitespace are not kept.

  • Every modeled object keeps the members it does not model in Extra and writes them back.
  • A member whose value does not have the modeled shape, such as null, an empty identifier, or a number where a string is expected, stays in Extra verbatim rather than failing the decode. When a typed field is set, it wins over an Extra entry of the same name.
  • Polymorphic members keep their form: content as a string or parts, tool_choice as a string or an object, stop as a string or an array.
  • Numbers are json.Number, so 1.0 stays 1.0.
  • Optional identifiers are strings omitted when empty. Text that may be empty on purpose, such as streamed arguments, is a *string, and booleans are *bool. A nil slice is omitted, while an empty non-nil slice encodes as [].
Vendor fields

The fields products depend on are typed rather than left in Extra:

  • ChatToolCall.ExtraContent.Google.ThoughtSignature for Gemini's extra_content.google.thought_signature. The ChatToolCall.ThoughtSignature() helper returns the first non-empty signature from that field, then function.thought_signature, then a call-level thought_signature; the last two stay in Extra.
  • ChatMessage.ReasoningDetails, the raw reasoning_details array.
  • CacheControl on ChatContentPart, AnthropicBlock (content, system and nested tool_result blocks) and AnthropicTool.

License

MIT

Documentation

Overview

Package translate round-trips between the Anthropic Messages API and the OpenAI Chat Completions schema. The gateway speaks OpenAI natively; this is the single translator that lets Claude Code and other Anthropic clients talk to it, and lets the native Anthropic backend be called from OpenAI-shaped internals.

Faithful port of llmgw.api.anthropic_translate. JSON objects are modeled as map[string]any / []any to mirror the Python dict handling.

Index

Constants

This section is empty.

Variables

View Source
var ErrStreamTruncated = errors.New("Anthropic stream ended without message_stop")

ErrStreamTruncated is the error of a source that ended without message_stop and without an error event, such as a dropped connection.

Functions

func AnthropicMessagesToOpenAI

func AnthropicMessagesToOpenAI(messages []any, system any) []map[string]any

AnthropicMessagesToOpenAI converts Anthropic messages + system into OpenAI messages.

func AnthropicRequestToOpenAI

func AnthropicRequestToOpenAI(payload map[string]any) ([]map[string]any, map[string]any, []string)

AnthropicRequestToOpenAI strictly converts the Messages core profile. The returned paths name every value that an OpenAI Chat target would lose.

func AnthropicResponseToOpenAI

func AnthropicResponseToOpenAI(response map[string]any, model string) map[string]any

AnthropicResponseToOpenAI converts a non-streaming Anthropic Message to an OpenAI completion.

func AnthropicSSEToOpenAIChunks

func AnthropicSSEToOpenAIChunks(lines func() (string, bool), model string, emit func(string))

AnthropicSSEToOpenAIChunks translates Anthropic SSE data lines into OpenAI chat.completion.chunk JSON strings, invoking emit for each. It emits no usage and ends every stream with a finish chunk, even one that failed or was cut short; AnthropicSSEToOpenAIChunksWithOptions reports both.

func AnthropicStopReasonToOpenAI

func AnthropicStopReasonToOpenAI(reason any) string

AnthropicStopReasonToOpenAI maps an Anthropic stop_reason to an OpenAI finish_reason.

func AnthropicToolChoiceToOpenAI

func AnthropicToolChoiceToOpenAI(choice map[string]any) any

AnthropicToolChoiceToOpenAI converts Anthropic tool_choice to OpenAI's.

func AnthropicToolsToOpenAI

func AnthropicToolsToOpenAI(tools []any) []any

AnthropicToolsToOpenAI converts Anthropic tools to OpenAI function tools.

func ChatResponseToResponses

func ChatResponseToResponses(model string, chat map[string]any) map[string]any

ChatResponseToResponses converts the gateway's internal Chat Completions result back into the public Responses API envelope.

func ChatResponseToResponsesWithRequest

func ChatResponseToResponsesWithRequest(
	model string, chat map[string]any, request map[string]any,
) map[string]any

func ChatToResponses

func ChatToResponses(model string, messages []map[string]any, kw map[string]any, stream bool) map[string]any

ChatToResponses converts a Chat Completions request body into a Responses API request body.

func FailedResponseEnvelope

func FailedResponseEnvelope(
	model, id, code, message string, request map[string]any,
) map[string]any

func NewResponseEnvelope

func NewResponseEnvelope(
	model, id, status string, output []any, request map[string]any,
) map[string]any

NewResponseEnvelope creates the schema-complete Response shape used whenever the gateway adapts a Chat-only backend or synthesizes a terminal failure.

func OpenAIFinishReasonToAnthropic

func OpenAIFinishReasonToAnthropic(reason any) string

OpenAIFinishReasonToAnthropic maps an OpenAI finish_reason to Anthropic stop_reason.

func OpenAIMessagesToAnthropic

func OpenAIMessagesToAnthropic(messages []map[string]any) (string, []map[string]any)

OpenAIMessagesToAnthropic splits an OpenAI message list into Anthropic (system, messages). cache_control on message content parts is carried to the Anthropic blocks; cache_control on system parts needs OpenAIMessagesToAnthropicWithReport, because the system returned here is a plain string.

func OpenAIResponseToAnthropic

func OpenAIResponseToAnthropic(response map[string]any, model string) map[string]any

OpenAIResponseToAnthropic converts a non-streaming OpenAI completion to an Anthropic Message.

func OpenAIStreamToAnthropicSSE

func OpenAIStreamToAnthropicSSE(chunks func() (string, bool), model string, emit func(string))

OpenAIStreamToAnthropicSSE translates OpenAI SSE chunk payloads (JSON strings, data: prefix already stripped) into Anthropic SSE event blocks, invoking emit for each output event.

func OpenAIToolsToAnthropic

func OpenAIToolsToAnthropic(tools []any) []any

OpenAIToolsToAnthropic converts OpenAI tools to Anthropic tool shape.

func PreferredEndpoint

func PreferredEndpoint(supportedEndpoints []string) string

PreferredEndpoint decides which OpenAI-family endpoint a model should use given its published supported_surfaces. Returns "responses" only when the model advertises /responses but NOT /chat/completions; otherwise "chat" (the safe default, including when the list is empty/unknown).

func RejectMaterialLoss added in v0.2.0

func RejectMaterialLoss(report Report) error

RejectMaterialLoss returns an error containing every material loss in the report.

func ResponsesRequestToChat

func ResponsesRequestToChat(
	payload map[string]any,
) ([]map[string]any, map[string]any, error)

ResponsesRequestToChat converts supported Responses input/history/tools into the gateway's internal Chat representation. Built-in Responses-only tools fail explicitly instead of being silently flattened.

func ResponsesToChat

func ResponsesToChat(model string, resp map[string]any) map[string]any

ResponsesToChat converts a Responses API object into a chat.completion.

func ResponsesToChatChunks

func ResponsesToChatChunks(model string, resp map[string]any) []string

ResponsesToChatChunks renders a (buffered) Responses reply as a small sequence of chat.completion.chunk payloads for the streaming code path.

Types

type AnthropicMessages added in v0.2.0

type AnthropicMessages struct {
	System   string
	Messages []map[string]any
	// SystemBlocks is set when a system or developer text part carries
	// cache_control. It holds the System text as Anthropic text blocks split
	// at the breakpoints, each carrying its cache_control. Send it as the
	// request's system value instead of System, or the breakpoints are lost.
	SystemBlocks []any `json:",omitempty"`
}

AnthropicMessages is the two-part request value returned by Anthropic conversions.

type ConversionResult added in v0.2.0

type ConversionResult[T any] struct {
	Value  T      `json:"value"`
	Report Report `json:"report"`
}

ConversionResult couples an existing conversion value to its compatibility report.

func AnthropicRequestToOpenAIWithReport added in v0.2.0

func AnthropicRequestToOpenAIWithReport(payload map[string]any) ConversionResult[OpenAIChatRequest]

AnthropicRequestToOpenAIWithReport delegates to AnthropicRequestToOpenAI and gives its incompatible paths structured material-loss semantics.

func AnthropicResponseToOpenAIWithReport added in v0.2.0

func AnthropicResponseToOpenAIWithReport(response map[string]any, model string) ConversionResult[map[string]any]

AnthropicResponseToOpenAIWithReport converts an Anthropic response with fidelity findings.

func ChatResponseToResponsesWithReport added in v0.2.0

func ChatResponseToResponsesWithReport(model string, chat map[string]any) ConversionResult[map[string]any]

ChatResponseToResponsesWithReport converts a Chat response with fidelity findings.

func ChatResponseToResponsesWithRequestAndReport added in v0.2.0

func ChatResponseToResponsesWithRequestAndReport(model string, chat map[string]any, request map[string]any) ConversionResult[map[string]any]

ChatResponseToResponsesWithRequestAndReport also uses request fields when constructing the Responses envelope.

func ChatToResponsesWithReport added in v0.2.0

func ChatToResponsesWithReport(model string, messages []map[string]any, kw map[string]any, stream bool) ConversionResult[map[string]any]

ChatToResponsesWithReport converts a Chat request and reports known fallback synthesis.

func OpenAIMessagesToAnthropicWithReport added in v0.2.0

func OpenAIMessagesToAnthropicWithReport(messages []map[string]any) ConversionResult[AnthropicMessages]

OpenAIMessagesToAnthropicWithReport converts messages and reports known normalizations performed by the existing converter. cache_control on text parts is carried, including on system parts through SystemBlocks; vendor fields Anthropic cannot represent are reported at their paths.

func OpenAIResponseToAnthropicWithReport added in v0.2.0

func OpenAIResponseToAnthropicWithReport(response map[string]any, model string) ConversionResult[map[string]any]

OpenAIResponseToAnthropicWithReport converts a Chat response with fidelity findings.

func ResponsesRequestToChatWithReport added in v0.2.0

func ResponsesRequestToChatWithReport(payload map[string]any) (ConversionResult[OpenAIChatRequest], error)

ResponsesRequestToChatWithReport preserves existing validation errors and reports unsupported top-level fields as material compatibility loss.

func ResponsesToChatChunksWithReport added in v0.2.0

func ResponsesToChatChunksWithReport(model string, response map[string]any) ConversionResult[[]string]

ResponsesToChatChunksWithReport reports that a complete response is rendered as buffered chunks rather than preserving original event boundaries.

func ResponsesToChatWithReport added in v0.2.0

func ResponsesToChatWithReport(model string, response map[string]any) ConversionResult[map[string]any]

ResponsesToChatWithReport converts a Responses object with fidelity findings.

func (ConversionResult[T]) RejectMaterialLoss added in v0.2.0

func (r ConversionResult[T]) RejectMaterialLoss() error

RejectMaterialLoss returns an error when this result contains material loss.

type Loss added in v0.2.0

type Loss struct {
	Path     string       `json:"path"`
	Class    LossClass    `json:"class"`
	Severity LossSeverity `json:"severity"`
	Detail   string       `json:"detail"`
}

Loss is one deterministic, machine-readable compatibility finding.

type LossClass added in v0.2.0

type LossClass string

LossClass describes how a source value differs after conversion.

const (
	LossUnsupported  LossClass = "unsupported"
	LossDropped      LossClass = "dropped"
	LossApproximated LossClass = "approximated"
	LossRenamed      LossClass = "renamed"
)

type LossSeverity added in v0.2.0

type LossSeverity string

LossSeverity distinguishes fidelity failures from useful compatibility notes.

const (
	LossMaterial LossSeverity = "material"
	LossAdvisory LossSeverity = "advisory"
)

type MaterialLossError added in v0.2.0

type MaterialLossError struct {
	Losses []Loss
}

MaterialLossError contains the material subset of a rejected report.

func (*MaterialLossError) Error added in v0.2.0

func (e *MaterialLossError) Error() string

type OpenAIChatRequest added in v0.2.0

type OpenAIChatRequest struct {
	Messages []map[string]any
	Keywords map[string]any
}

OpenAIChatRequest is the two-part request value returned by Chat conversions.

type Report added in v0.2.0

type Report struct {
	Losses []Loss `json:"losses,omitempty"`
}

Report describes all known compatibility losses for one conversion. Losses are sorted by path, severity, class, then detail.

func AnthropicSSEToOpenAIChunksWithReport added in v0.2.0

func AnthropicSSEToOpenAIChunksWithReport(lines func() (string, bool), model string, emit func(string)) Report

AnthropicSSEToOpenAIChunksWithReport delegates stream conversion and reports usage fields that the existing Chat chunk adapter does not emit, and the thinking blocks it drops.

func NewReport added in v0.2.0

func NewReport(losses ...Loss) Report

NewReport normalizes ordering and removes duplicate losses.

func OpenAIStreamToAnthropicSSEWithReport added in v0.2.0

func OpenAIStreamToAnthropicSSEWithReport(chunks func() (string, bool), model string, emit func(string)) Report

OpenAIStreamToAnthropicSSEWithReport delegates stream conversion and reports the token estimate used when chunks do not carry usage, and the vendor fields of deltas that Anthropic events cannot carry.

func (Report) HasMaterialLoss added in v0.2.0

func (r Report) HasMaterialLoss() bool

HasMaterialLoss reports whether conversion changed or omitted material semantics.

type StreamError added in v0.4.0

type StreamError struct {
	Type    string // Anthropic's error type, such as overloaded_error
	Message string
}

StreamError is an error event received from an Anthropic stream, such as an overloaded_error after the response began.

func (*StreamError) Error added in v0.4.0

func (e *StreamError) Error() string

type StreamOptions added in v0.4.0

type StreamOptions struct {
	// IncludeUsage emits what OpenAI sends for stream_options.include_usage:
	// after the finish chunk, a chunk with empty choices and the usage of the
	// whole request, and "usage": null on every other chunk. The usage chunk
	// is sent only for a completed stream whose source reported usage, so it
	// never carries a partial or invented count.
	IncludeUsage bool
}

StreamOptions selects optional output of AnthropicSSEToOpenAIChunksWithOptions.

type StreamReport added in v0.4.0

type StreamReport struct {
	// Usage holds the latest count of each kind the source reported, also
	// for a stream that failed, and is zero when the source reported none.
	Usage Usage
	// Completed is true when message_stop arrived and no error event did.
	Completed bool
	// Err is nil when Completed. Otherwise it is the *StreamError of the
	// first error event, or ErrStreamTruncated.
	Err error
}

StreamReport tells how a translated stream ended.

func AnthropicSSEToOpenAIChunksWithOptions added in v0.4.0

func AnthropicSSEToOpenAIChunksWithOptions(lines func() (string, bool), model string, emit func(string), opts StreamOptions) *StreamReport

AnthropicSSEToOpenAIChunksWithOptions translates like AnthropicSSEToOpenAIChunks and, once lines is exhausted, returns a report that is never nil.

Without options, a completed stream gets exactly the chunks AnthropicSSEToOpenAIChunks emits; StreamOptions describes what IncludeUsage adds. Any other stream ends where its source did, without a finish chunk, so a failed or truncated answer cannot pass for a complete one. Its chunks all come from source events: when an error arrives first, nothing has been emitted and the caller can still respond with an error status.

type Usage added in v0.4.0

type Usage struct {
	InputTokens              int `json:"input_tokens"`
	CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
	CacheReadInputTokens     int `json:"cache_read_input_tokens"`
	OutputTokens             int `json:"output_tokens"`
}

Usage holds the token counts an Anthropic stream reported. Anthropic counts cache writes and cache reads apart from InputTokens, so the Chat prompt_tokens is the sum of all three.

func (Usage) PromptTokens added in v0.4.0

func (u Usage) PromptTokens() int

PromptTokens returns the Chat prompt_tokens, which also counts the input read from or written to the cache.

func (Usage) TotalTokens added in v0.4.0

func (u Usage) TotalTokens() int

TotalTokens returns the Chat total_tokens.

Directories

Path Synopsis
Package wire holds typed request, response and stream-chunk structs for the OpenAI Chat Completions, OpenAI Responses and Anthropic Messages surfaces.
Package wire holds typed request, response and stream-chunk structs for the OpenAI Chat Completions, OpenAI Responses and Anthropic Messages surfaces.

Jump to

Keyboard shortcuts

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