| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961 |
- package apicompat
- import (
- "encoding/json"
- "fmt"
- "sort"
- "strings"
- "time"
- )
- // This file implements a DIRECT bridge between Anthropic Messages and OpenAI
- // Chat Completions, skipping the Responses API intermediate representation.
- //
- // The existing chat-fallback path (forwardAnthropicViaRawChatCompletions) chains
- // two Responses-anchored bridges — Anthropic→Responses→ChatCompletions on the
- // request side and CC→Responses→Anthropic on the response side — so every
- // streaming token runs through two state machines. For force-chat accounts
- // (third-party OpenAI-compatible upstreams that only speak /v1/chat/completions)
- // the Responses layer is pure overhead: these upstreams never see Responses
- // semantics, and the clients reaching them via /v1/messages use standard
- // function tools (no custom/tool_search/namespace Codex constructs).
- //
- // The direct bridge collapses both directions into a single conversion each:
- //
- // Request: Anthropic Messages → Chat Completions
- // Response: CC chunk/response → Anthropic events/response
- //
- // Helper functions from the Responses bridges (anthropicImageToDataURI,
- // extractAnthropicTextFromBlocks, fromResponsesCallID, sanitizeAnthropicToolUseInput,
- // parseAnthropicSystemContentParts, isReasoningModel, mapAnthropicEffortToResponses,
- // normalizeToolParameters) are reused so the conversion semantics stay identical.
- // ---------------------------------------------------------------------------
- // Request: AnthropicRequest → ChatCompletionsRequest
- // ---------------------------------------------------------------------------
- // AnthropicToChatCompletionsRequest converts an Anthropic Messages request
- // directly into a Chat Completions request, without transiting the Responses
- // API. It is semantically equivalent to composing AnthropicToResponses +
- // ResponsesToChatCompletionsRequest but avoids materializing the intermediate
- // ResponsesRequest and the extra marshal/unmarshal cycle.
- func AnthropicToChatCompletionsRequest(req *AnthropicRequest) (*ChatCompletionsRequest, error) {
- if req == nil {
- return nil, fmt.Errorf("anthropic request is nil")
- }
- messages, err := anthropicToChatMessages(req.System, req.Messages)
- if err != nil {
- return nil, err
- }
- out := &ChatCompletionsRequest{
- Model: req.Model,
- Messages: messages,
- Stream: req.Stream,
- }
- // Sampling params: reasoning models (gpt-5.x) reject temperature/top_p.
- if !isReasoningModel(req.Model) {
- out.Temperature = req.Temperature
- out.TopP = req.TopP
- }
- if req.MaxTokens > 0 {
- v := req.MaxTokens
- if v < minMaxOutputTokens {
- v = minMaxOutputTokens
- }
- out.MaxCompletionTokens = &v
- }
- // Tools: Anthropic input_schema is a JSON Schema, directly usable as Chat
- // function parameters. Server tools (web_search_*) have no Chat Completions
- // equivalent and are dropped (mirrors responsesToolsToChatTools).
- if len(req.Tools) > 0 {
- tools := anthropicToolsToChatTools(req.Tools)
- if len(tools) > 0 {
- out.Tools = tools
- }
- }
- // tool_choice is only forwarded when tools survived the conversion
- // (upstream rejects tool_choice without tools), and a named choice only when
- // it points at a declared tool — mirroring responsesToolChoiceToChatToolChoice,
- // chat upstreams 400 on tool_choice referencing an unknown tool.
- if len(out.Tools) > 0 && len(req.ToolChoice) > 0 {
- declared := make(map[string]bool, len(out.Tools))
- for _, tool := range out.Tools {
- if tool.Function != nil {
- declared[tool.Function.Name] = true
- }
- }
- tc, err := convertAnthropicToolChoiceToChat(req.ToolChoice, declared)
- if err != nil {
- return nil, fmt.Errorf("convert tool_choice: %w", err)
- }
- if len(tc) > 0 {
- out.ToolChoice = tc
- }
- }
- // Reasoning effort: output_config.effort maps 1:1 (max→xhigh). thinking.type
- // itself is ignored (the Responses bridge behaves identically).
- effort := "medium"
- if req.OutputConfig != nil && req.OutputConfig.Effort != "" {
- effort = req.OutputConfig.Effort
- }
- out.ReasoningEffort = mapAnthropicEffortToResponses(effort)
- parallelToolCalls := true
- out.ParallelToolCalls = ¶llelToolCalls
- return out, nil
- }
- // anthropicToChatMessages converts the Anthropic system field + message list
- // into Chat Completions messages. It mirrors convertAnthropicToResponsesInput +
- // responsesInputToChatMessages but produces ChatMessage directly.
- func anthropicToChatMessages(system json.RawMessage, msgs []AnthropicMessage) ([]ChatMessage, error) {
- var messages []ChatMessage
- // System prompt → system message. parseAnthropicSystemContentParts handles
- // both string and []block forms and filters the billing header.
- if len(system) > 0 {
- sysParts, err := parseAnthropicSystemContentParts(system)
- if err != nil {
- return nil, err
- }
- if len(sysParts) > 0 {
- text := joinResponsesContentPartText(sysParts)
- if text != "" {
- content, _ := json.Marshal(text)
- messages = append(messages, ChatMessage{Role: "system", Content: content})
- }
- }
- }
- for _, m := range msgs {
- converted, err := anthropicMsgToChatMessages(m)
- if err != nil {
- return nil, err
- }
- messages = append(messages, converted...)
- }
- return normalizeChatMessages(messages), nil
- }
- // anthropicMsgToChatMessages converts one Anthropic message into one or more
- // Chat messages. tool_result blocks become standalone "tool" role messages
- // (the Chat Completions convention); text/image blocks stay in a user message;
- // assistant tool_use blocks become tool_calls on the assistant message.
- func anthropicMsgToChatMessages(m AnthropicMessage) ([]ChatMessage, error) {
- switch m.Role {
- case "assistant":
- return anthropicAssistantToChatMessages(m.Content)
- default: // "user" and any unknown role
- return anthropicUserToChatMessages(m.Content)
- }
- }
- // anthropicUserToChatMessages handles an Anthropic user message. Content may be
- // a plain string or an array of blocks. tool_result blocks are extracted into
- // standalone "tool" role messages; images inside tool_results are lifted into a
- // follow-up user message as image_url parts (the Responses bridge does the same
- // — function_call_output only accepts strings, so images must travel separately).
- func anthropicUserToChatMessages(raw json.RawMessage) ([]ChatMessage, error) {
- // Plain string → single user message.
- var s string
- if err := json.Unmarshal(raw, &s); err == nil {
- content, _ := json.Marshal(s)
- return []ChatMessage{{Role: "user", Content: content}}, nil
- }
- var blocks []AnthropicContentBlock
- if err := json.Unmarshal(raw, &blocks); err != nil {
- return nil, err
- }
- var out []ChatMessage
- var toolResultImageParts []ChatContentPart
- // tool_result → "tool" role messages, text extracted; images deferred.
- for _, b := range blocks {
- if b.Type != "tool_result" {
- continue
- }
- text, imageParts := convertToolResultOutput(b)
- content, _ := json.Marshal(text)
- out = append(out, ChatMessage{
- Role: "tool",
- Content: content,
- ToolCallID: b.ToolUseID,
- })
- for _, ip := range imageParts {
- toolResultImageParts = append(toolResultImageParts, ChatContentPart{
- Type: "image_url",
- ImageURL: &ChatImageURL{URL: ip.ImageURL},
- })
- }
- }
- // Remaining text + image blocks → user message. The double-conversion path
- // (responsesContentPartsToChatContent) folds text-only content into a single
- // string joined with "\n\n" and only uses the parts-array form when an image
- // is present — strict chat upstreams reject array content — so the direct
- // bridge preserves that folding.
- var textParts []string
- var parts []ChatContentPart
- hasImage := false
- for _, b := range blocks {
- switch b.Type {
- case "text":
- if b.Text != "" {
- textParts = append(textParts, b.Text)
- parts = append(parts, ChatContentPart{Type: "text", Text: b.Text})
- }
- case "image":
- if uri := anthropicImageToDataURI(b.Source); uri != "" {
- hasImage = true
- parts = append(parts, ChatContentPart{
- Type: "image_url",
- ImageURL: &ChatImageURL{URL: uri},
- })
- }
- }
- }
- if len(toolResultImageParts) > 0 {
- hasImage = true
- parts = append(parts, toolResultImageParts...)
- }
- if !hasImage {
- if len(textParts) > 0 {
- content, _ := json.Marshal(strings.Join(textParts, "\n\n"))
- out = append(out, ChatMessage{Role: "user", Content: content})
- }
- return out, nil
- }
- content, err := json.Marshal(parts)
- if err != nil {
- return nil, err
- }
- out = append(out, ChatMessage{Role: "user", Content: content})
- return out, nil
- }
- // anthropicAssistantToChatMessages handles an Anthropic assistant message.
- // Text content → assistant message content; tool_use blocks → tool_calls on the
- // same assistant message; thinking blocks → reasoning_content, but only on a
- // message that carries tool calls (see anthropicThinkingToReasoningContent).
- func anthropicAssistantToChatMessages(raw json.RawMessage) ([]ChatMessage, error) {
- // Plain string → single assistant message.
- var s string
- if err := json.Unmarshal(raw, &s); err == nil {
- content, _ := json.Marshal(s)
- return []ChatMessage{{Role: "assistant", Content: content}}, nil
- }
- var blocks []AnthropicContentBlock
- if err := json.Unmarshal(raw, &blocks); err != nil {
- return nil, err
- }
- msg := ChatMessage{Role: "assistant"}
- text := extractAnthropicTextFromBlocks(blocks)
- if text != "" {
- content, _ := json.Marshal(text)
- msg.Content = content
- }
- for _, b := range blocks {
- if b.Type != "tool_use" {
- continue
- }
- args := "{}"
- if len(b.Input) > 0 {
- args = string(b.Input)
- }
- msg.ToolCalls = append(msg.ToolCalls, ChatToolCall{
- ID: b.ID,
- Type: "function",
- Function: ChatFunctionCall{
- Name: b.Name,
- Arguments: args,
- },
- })
- }
- msg.ReasoningContent = anthropicThinkingToReasoningContent(blocks, len(msg.ToolCalls) > 0)
- return []ChatMessage{msg}, nil
- }
- // anthropicThinkingToReasoningContent folds thinking blocks back into the
- // Chat Completions reasoning_content field.
- //
- // chatMessageToAnthropicBlocks emits the upstream's reasoning_content as a
- // thinking block on the way out, so a multi-turn client echoes it back on the
- // next request; dropping it here made the bridge lose exactly what it had just
- // produced. DeepSeek's thinking mode requires the reasoning_content that
- // produced a tool call to be replayed on that assistant message and answers
- // 400 otherwise, which is why buildChatMessagesFromItems already carries
- // pendingReasoning onto assistant tool-call messages in the Responses→Chat
- // bridge. hasToolCalls keeps the scope identical to that sibling: reasoning
- // rides along with tool calls only, never on a plain assistant text turn.
- //
- // redacted_thinking blocks and signature-only placeholders carry no plaintext
- // and contribute nothing. Multiple blocks join with "\n", matching
- // extractResponsesReasoningText.
- func anthropicThinkingToReasoningContent(blocks []AnthropicContentBlock, hasToolCalls bool) string {
- if !hasToolCalls {
- return ""
- }
- var parts []string
- for _, b := range blocks {
- if b.Type == "thinking" && b.Thinking != "" {
- parts = append(parts, b.Thinking)
- }
- }
- return strings.Join(parts, "\n")
- }
- // anthropicToolsToChatTools maps Anthropic tool definitions to Chat Completions
- // function tools. Server-side tools (web_search_*) are dropped — they have no
- // Chat Completions equivalent.
- func anthropicToolsToChatTools(tools []AnthropicTool) []ChatTool {
- var out []ChatTool
- for _, t := range tools {
- if strings.HasPrefix(t.Type, "web_search") {
- continue
- }
- out = append(out, ChatTool{
- Type: "function",
- Function: &ChatFunction{
- Name: t.Name,
- Description: t.Description,
- Parameters: normalizeToolParameters(t.InputSchema),
- Strict: boolPtr(false),
- },
- })
- }
- return out
- }
- // convertAnthropicToolChoiceToChat maps Anthropic tool_choice to Chat
- // Completions tool_choice. A nil result means the choice is dropped: like the
- // double-conversion path (responsesToolChoiceToChatToolChoice), a named choice
- // pointing at an undeclared tool or an unknown choice type is not forwarded,
- // because chat upstreams reject it.
- //
- // {"type":"auto"} → "auto"
- // {"type":"any"} → "required"
- // {"type":"none"} → "none"
- // {"type":"tool","name":"X"} → {"type":"function","function":{"name":"X"}} (X declared)
- func convertAnthropicToolChoiceToChat(raw json.RawMessage, declared map[string]bool) (json.RawMessage, error) {
- var tc struct {
- Type string `json:"type"`
- Name string `json:"name"`
- }
- if err := json.Unmarshal(raw, &tc); err != nil {
- return nil, err
- }
- switch tc.Type {
- case "auto":
- return json.Marshal("auto")
- case "any":
- return json.Marshal("required")
- case "none":
- return json.Marshal("none")
- case "tool":
- if tc.Name == "" || !declared[tc.Name] {
- return nil, nil
- }
- return json.Marshal(map[string]any{
- "type": "function",
- "function": map[string]string{"name": tc.Name},
- })
- default:
- return nil, nil
- }
- }
- // joinResponsesContentPartText concatenates the text of input_text parts. Used
- // for the system prompt where parseAnthropicSystemContentParts returns
- // ResponsesContentPart values.
- func joinResponsesContentPartText(parts []ResponsesContentPart) string {
- var texts []string
- for _, p := range parts {
- if p.Type == "input_text" && p.Text != "" {
- texts = append(texts, p.Text)
- }
- }
- return strings.Join(texts, "\n\n")
- }
- // ---------------------------------------------------------------------------
- // Non-streaming response: ChatCompletionsResponse → AnthropicResponse
- // ---------------------------------------------------------------------------
- // ChatCompletionsResponseToAnthropic converts a Chat Completions response
- // directly into an Anthropic Messages response, without materializing a
- // ResponsesResponse. It is semantically equivalent to composing
- // ChatCompletionsResponseToResponses + ResponsesToAnthropic.
- func ChatCompletionsResponseToAnthropic(resp *ChatCompletionsResponse, model string) *AnthropicResponse {
- out := &AnthropicResponse{
- Type: "message",
- Role: "assistant",
- Model: model,
- }
- if resp != nil {
- out.ID = resp.ID
- if out.Model == "" {
- out.Model = resp.Model
- }
- if len(resp.Choices) > 0 {
- choice := resp.Choices[0]
- out.Content = chatMessageToAnthropicBlocks(choice.Message)
- out.StopReason = AnthropicStopReasonPtr(chatFinishReasonToAnthropicStopReason(choice.FinishReason, out.Content))
- // "length" → "max_tokens" is handled by chatFinishReasonToAnthropicStopReason;
- // Anthropic conveys max-tokens via stop_reason only, no incomplete_details field.
- }
- if resp.Usage != nil {
- out.Usage = chatUsageToAnthropicUsage(resp.Usage)
- }
- }
- if len(out.Content) == 0 {
- out.Content = []AnthropicContentBlock{{Type: "text", Text: ""}}
- }
- // Empty choices / nil response never enter the choices branch above; the
- // double-conversion path still reports a completed turn ("end_turn"), and
- // stop_reason null/"" is invalid for completed non-stream responses.
- if AnthropicStopReasonString(out.StopReason) == "" {
- out.StopReason = AnthropicStopReasonPtr(chatFinishReasonToAnthropicStopReason("", out.Content))
- }
- // The double-conversion path generates a response id when the upstream
- // omits one (ChatCompletionsResponseToResponses); clients treat it as required.
- if out.ID == "" {
- out.ID = generateResponsesID()
- }
- return out
- }
- // chatMessageToAnthropicBlocks converts a Chat Completions message into
- // Anthropic content blocks. Reasoning content → thinking block; text content →
- // text block; tool_calls → tool_use blocks. Mirrors chatMessageToResponsesOutput
- // + the reasoning→thinking mapping in ResponsesToAnthropic.
- func chatMessageToAnthropicBlocks(message ChatMessage) []AnthropicContentBlock {
- var blocks []AnthropicContentBlock
- reasoning := message.reasoningText()
- if reasoning != "" {
- blocks = append(blocks, AnthropicContentBlock{
- Type: "thinking",
- Thinking: reasoning,
- })
- }
- text := chatMessageContentText(message.Content)
- // DeepSeek reasoning-only fallback: when there is no text and no tool calls,
- // surface the reasoning content as visible text so the turn isn't empty.
- if text == "" && strings.TrimSpace(reasoning) != "" && len(message.ToolCalls) == 0 {
- text = reasoning
- }
- if text != "" || len(message.ToolCalls) == 0 {
- blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: text})
- }
- for _, toolCall := range message.ToolCalls {
- arguments := toolCall.Function.Arguments
- if strings.TrimSpace(arguments) == "" {
- arguments = "{}"
- }
- blocks = append(blocks, AnthropicContentBlock{
- Type: "tool_use",
- ID: fromResponsesCallID(toolCall.ID),
- Name: toolCall.Function.Name,
- Input: sanitizeAnthropicToolUseInput(toolCall.Function.Name, arguments),
- })
- }
- return blocks
- }
- // chatFinishReasonToAnthropicStopReason maps Chat Completions finish_reason to
- // Anthropic stop_reason.
- //
- // "length" → "max_tokens"
- // "tool_calls" → "tool_use"
- // other → "end_turn" (or "tool_use" if tool_use blocks present)
- //
- // "stop", "content_filter", and unknown reasons all map to a completed response
- // in the double-conversion path, which then derives stop_reason from the blocks.
- func chatFinishReasonToAnthropicStopReason(reason string, blocks []AnthropicContentBlock) string {
- switch reason {
- case "length":
- return "max_tokens"
- case "tool_calls":
- return "tool_use"
- default:
- if containsAnthropicToolUseBlock(blocks) {
- return "tool_use"
- }
- return "end_turn"
- }
- }
- // chatUsageToAnthropicUsage converts Chat Completions token usage to Anthropic
- // usage shape. Mirrors ChatUsageToResponsesUsage + anthropicUsageFromResponsesUsage.
- func chatUsageToAnthropicUsage(usage *ChatUsage) AnthropicUsage {
- if usage == nil {
- return AnthropicUsage{}
- }
- cachedTokens := 0
- cacheCreationTokens := 0
- if usage.PromptTokensDetails != nil {
- cachedTokens = usage.PromptTokensDetails.CachedTokens
- // cache_write_tokens and cache_creation_tokens are alternate spellings of
- // the same quantity, not additive; the double-conversion path
- // (ChatUsageToResponsesUsage) prefers write and falls back to creation.
- if usage.PromptTokensDetails.CacheWriteTokens > 0 {
- cacheCreationTokens = usage.PromptTokensDetails.CacheWriteTokens
- } else {
- cacheCreationTokens = usage.PromptTokensDetails.CacheCreationTokens
- }
- }
- inputTokens := usage.PromptTokens - cachedTokens - cacheCreationTokens
- if inputTokens < 0 {
- inputTokens = 0
- }
- return AnthropicUsage{
- InputTokens: inputTokens,
- OutputTokens: usage.CompletionTokens,
- CacheReadInputTokens: cachedTokens,
- CacheCreationInputTokens: cacheCreationTokens,
- }
- }
- // ---------------------------------------------------------------------------
- // Streaming: ChatCompletionsChunk → []AnthropicStreamEvent (stateful converter)
- // ---------------------------------------------------------------------------
- // ChatCompletionsToAnthropicStreamState tracks state while converting Chat
- // Completions SSE chunks directly into Anthropic SSE events. It collapses the
- // ChatCompletionsToResponsesStreamState + ResponsesEventToAnthropicState pair
- // into one state machine.
- type ChatCompletionsToAnthropicStreamState struct {
- MessageStartSent bool
- MessageStopSent bool
- // Current content block lifecycle.
- ContentBlockIndex int
- ContentBlockOpen bool
- CurrentBlockType string // "text" | "thinking" | "tool_use"
- CurrentToolName string
- CurrentToolHadDelta bool
- HasToolCall bool
- // Tool calls keyed by the upstream tool_call index. The Anthropic block
- // index is assigned when the tool block is announced (content_block_start),
- // which is deferred until the tool's name has arrived. Argument fragments
- // and the call ID seen before the name are buffered and flushed with the
- // announcement; tools whose name never arrives are announced with an empty
- // name at finalize so their arguments are not lost.
- toolBlockIndex map[int]int
- toolAnnounced map[int]bool
- toolName map[int]string
- pendingToolCallID map[int]string
- pendingToolArgs map[int]string
- // Reasoning (DeepSeek-style): reasoning_content streamed before content.
- // No separate reasoning block index — it uses ContentBlockIndex like the
- // Responses bridge's ReasoningIndex, but since blocks are sequential we
- // reuse the single ContentBlockIndex counter.
- FinishReason string
- InputTokens int
- OutputTokens int
- CacheReadInputTokens int
- CacheCreationInputTokens int
- ResponseID string
- Model string
- Created int64
- }
- // NewChatCompletionsToAnthropicStreamState returns an initialized stream state.
- func NewChatCompletionsToAnthropicStreamState(model string) *ChatCompletionsToAnthropicStreamState {
- return &ChatCompletionsToAnthropicStreamState{
- ResponseID: generateResponsesID(),
- Model: model,
- Created: time.Now().Unix(),
- toolBlockIndex: make(map[int]int),
- toolAnnounced: make(map[int]bool),
- toolName: make(map[int]string),
- pendingToolCallID: make(map[int]string),
- pendingToolArgs: make(map[int]string),
- }
- }
- // ChatCompletionsChunkToAnthropicEvents converts one Chat Completions stream
- // chunk into zero or more Anthropic stream events, updating state as it goes.
- func ChatCompletionsChunkToAnthropicEvents(
- chunk *ChatCompletionsChunk,
- state *ChatCompletionsToAnthropicStreamState,
- ) []AnthropicStreamEvent {
- if chunk == nil || state == nil {
- return nil
- }
- if chunk.ID != "" {
- state.ResponseID = chunk.ID
- }
- if state.Model == "" && chunk.Model != "" {
- state.Model = chunk.Model
- }
- // Usage in a streaming chunk (include_usage) arrives in its own chunk,
- // often with empty choices. Capture it for the finalize message_delta.
- if chunk.Usage != nil {
- u := chatUsageToAnthropicUsage(chunk.Usage)
- state.InputTokens = u.InputTokens
- state.OutputTokens = u.OutputTokens
- state.CacheReadInputTokens = u.CacheReadInputTokens
- state.CacheCreationInputTokens = u.CacheCreationInputTokens
- }
- var events []AnthropicStreamEvent
- events = append(events, ensureCCAnthropicMessageStart(state)...)
- for _, choice := range chunk.Choices {
- // Reasoning content → thinking block.
- reasoning := choice.Delta.reasoningText()
- if reasoning != nil && *reasoning != "" {
- events = append(events, ensureCCAnthropicThinkingBlock(state)...)
- events = append(events, ccAnthropicDelta(state, &AnthropicDelta{
- Type: "thinking_delta",
- Thinking: *reasoning,
- })...)
- }
- // Text content → text block (closes any open thinking block first).
- if choice.Delta.Content != nil && *choice.Delta.Content != "" {
- events = append(events, closeCCAnthropicBlockIfOpen(state, "thinking")...)
- events = append(events, ensureCCAnthropicTextBlock(state)...)
- events = append(events, ccAnthropicDelta(state, &AnthropicDelta{
- Type: "text_delta",
- Text: *choice.Delta.Content,
- })...)
- }
- // Tool calls → tool_use blocks.
- for _, toolCall := range choice.Delta.ToolCalls {
- events = append(events, closeCCAnthropicBlockIfOpen(state, "thinking")...)
- events = append(events, handleCCAnthropicToolCall(state, &toolCall)...)
- }
- if choice.FinishReason != nil && *choice.FinishReason != "" {
- state.FinishReason = *choice.FinishReason
- }
- }
- return events
- }
- // FinalizeChatCompletionsAnthropicStream emits terminal Anthropic events
- // (close open blocks + message_delta + message_stop) when the stream ends.
- func FinalizeChatCompletionsAnthropicStream(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
- if state == nil || state.MessageStopSent {
- return nil
- }
- var events []AnthropicStreamEvent
- if !state.MessageStartSent {
- events = append(events, ensureCCAnthropicMessageStart(state)...)
- }
- // Announce tools whose name never arrived so their buffered arguments are
- // not silently dropped. The double-conversion path announced these
- // immediately with an empty name; the deferred announcement keeps that data
- // preservation while still delivering correct names when they do arrive.
- if len(state.pendingToolCallID) > 0 {
- idxs := make([]int, 0, len(state.pendingToolCallID))
- for idx := range state.pendingToolCallID {
- idxs = append(idxs, idx)
- }
- sort.Ints(idxs)
- for _, idx := range idxs {
- callID := state.pendingToolCallID[idx]
- events = append(events, closeCCAnthropicBlock(state)...)
- events = append(events, announceCCAnthropicToolBlock(state, idx, callID, "")...)
- }
- }
- events = append(events, closeCCAnthropicBlock(state)...)
- stopReason := ccFinishReasonToAnthropicStopReason(state.FinishReason, state.HasToolCall)
- events = append(events,
- AnthropicStreamEvent{
- Type: "message_delta",
- Delta: &AnthropicDelta{
- StopReason: stopReason,
- },
- Usage: &AnthropicUsage{
- InputTokens: state.InputTokens,
- OutputTokens: state.OutputTokens,
- CacheReadInputTokens: state.CacheReadInputTokens,
- CacheCreationInputTokens: state.CacheCreationInputTokens,
- },
- },
- AnthropicStreamEvent{Type: "message_stop"},
- )
- state.MessageStopSent = true
- return events
- }
- // ensureCCAnthropicMessageStart emits message_start on the first event.
- func ensureCCAnthropicMessageStart(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
- if state.MessageStartSent {
- return nil
- }
- state.MessageStartSent = true
- return []AnthropicStreamEvent{{
- Type: "message_start",
- Message: &AnthropicResponse{
- ID: state.ResponseID,
- Type: "message",
- Role: "assistant",
- Content: []AnthropicContentBlock{},
- Model: state.Model,
- StopReason: nil, // JSON null; never ""
- Usage: AnthropicUsage{InputTokens: 0, OutputTokens: 0},
- },
- }}
- }
- // ensureCCAnthropicThinkingBlock opens a thinking block if none is open.
- func ensureCCAnthropicThinkingBlock(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
- if state.ContentBlockOpen && state.CurrentBlockType == "thinking" {
- return nil
- }
- events := closeCCAnthropicBlock(state)
- idx := state.ContentBlockIndex
- state.ContentBlockOpen = true
- state.CurrentBlockType = "thinking"
- events = append(events, AnthropicStreamEvent{
- Type: "content_block_start",
- Index: &idx,
- ContentBlock: &AnthropicContentBlock{
- Type: "thinking",
- Thinking: "",
- },
- })
- return events
- }
- // ensureCCAnthropicTextBlock opens a text block if none is open.
- func ensureCCAnthropicTextBlock(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
- if state.ContentBlockOpen && state.CurrentBlockType == "text" {
- return nil
- }
- events := closeCCAnthropicBlock(state)
- idx := state.ContentBlockIndex
- state.ContentBlockOpen = true
- state.CurrentBlockType = "text"
- events = append(events, AnthropicStreamEvent{
- Type: "content_block_start",
- Index: &idx,
- ContentBlock: &AnthropicContentBlock{
- Type: "text",
- Text: "",
- },
- })
- return events
- }
- // handleCCAnthropicToolCall processes one upstream tool_call delta. The
- // content_block_start for a tool is deferred until its name has arrived (some
- // upstreams stream id/arguments before the name); argument fragments seen
- // before the announcement are buffered and flushed with it, later fragments
- // stream as input_json_delta on the tool's block.
- func handleCCAnthropicToolCall(state *ChatCompletionsToAnthropicStreamState, toolCall *ChatToolCall) []AnthropicStreamEvent {
- idx := 0
- if toolCall.Index != nil {
- idx = *toolCall.Index
- }
- var events []AnthropicStreamEvent
- if _, seen := state.toolAnnounced[idx]; !seen {
- // New tool call: it ends whatever block is currently streaming.
- events = append(events, closeCCAnthropicBlock(state)...)
- state.HasToolCall = true
- callID := toolCall.ID
- if callID == "" {
- callID = generateItemID()
- }
- if name := toolCall.Function.Name; name != "" {
- events = append(events, announceCCAnthropicToolBlock(state, idx, callID, name)...)
- } else {
- state.toolAnnounced[idx] = false
- state.pendingToolCallID[idx] = callID
- }
- } else if !state.toolAnnounced[idx] && toolCall.Function.Name != "" {
- // Deferred announcement: the name has arrived.
- callID := state.pendingToolCallID[idx]
- if toolCall.ID != "" {
- callID = toolCall.ID
- }
- events = append(events, closeCCAnthropicBlock(state)...)
- events = append(events, announceCCAnthropicToolBlock(state, idx, callID, toolCall.Function.Name)...)
- }
- // Argument fragment → input_json_delta on the tool's block once announced,
- // buffered until the deferred announcement otherwise.
- if toolCall.Function.Arguments != "" {
- if state.toolAnnounced[idx] {
- blockIdx := state.toolBlockIndex[idx]
- if state.ContentBlockOpen && blockIdx == state.ContentBlockIndex {
- state.CurrentToolHadDelta = true
- }
- events = append(events, AnthropicStreamEvent{
- Type: "content_block_delta",
- Index: &blockIdx,
- Delta: &AnthropicDelta{
- Type: "input_json_delta",
- PartialJSON: toolCall.Function.Arguments,
- },
- })
- } else {
- state.pendingToolArgs[idx] += toolCall.Function.Arguments
- }
- }
- return events
- }
- // announceCCAnthropicToolBlock assigns the next Anthropic block index to the
- // tool, emits its content_block_start, and flushes any argument fragments
- // buffered while the announcement was deferred.
- func announceCCAnthropicToolBlock(state *ChatCompletionsToAnthropicStreamState, idx int, callID, name string) []AnthropicStreamEvent {
- blockIdx := state.ContentBlockIndex
- state.toolBlockIndex[idx] = blockIdx
- state.toolAnnounced[idx] = true
- state.toolName[idx] = name
- state.CurrentToolName = name
- state.CurrentToolHadDelta = false
- state.ContentBlockOpen = true
- state.CurrentBlockType = "tool_use"
- delete(state.pendingToolCallID, idx)
- events := []AnthropicStreamEvent{{
- Type: "content_block_start",
- Index: &blockIdx,
- ContentBlock: &AnthropicContentBlock{
- Type: "tool_use",
- ID: fromResponsesCallID(callID),
- Name: name,
- Input: json.RawMessage("{}"),
- },
- }}
- if pending := state.pendingToolArgs[idx]; pending != "" {
- delete(state.pendingToolArgs, idx)
- state.CurrentToolHadDelta = true
- events = append(events, AnthropicStreamEvent{
- Type: "content_block_delta",
- Index: &blockIdx,
- Delta: &AnthropicDelta{
- Type: "input_json_delta",
- PartialJSON: pending,
- },
- })
- }
- return events
- }
- // ccAnthropicDelta emits a content_block_delta on the current block.
- func ccAnthropicDelta(state *ChatCompletionsToAnthropicStreamState, delta *AnthropicDelta) []AnthropicStreamEvent {
- if !state.ContentBlockOpen {
- return nil
- }
- idx := state.ContentBlockIndex
- return []AnthropicStreamEvent{{
- Type: "content_block_delta",
- Index: &idx,
- Delta: delta,
- }}
- }
- // closeCCAnthropicBlockIfOpen closes the current block only if it matches the
- // given type (used to close a thinking block before opening text/tool).
- func closeCCAnthropicBlockIfOpen(state *ChatCompletionsToAnthropicStreamState, blockType string) []AnthropicStreamEvent {
- if !state.ContentBlockOpen || state.CurrentBlockType != blockType {
- return nil
- }
- return closeCCAnthropicBlock(state)
- }
- // closeCCAnthropicBlock closes the currently open content block. A tool_use
- // block that streamed no argument delta gets a final input_json_delta "{}"
- // first — the double-conversion path normalizes empty tool arguments to "{}",
- // and some clients assemble tool input exclusively from deltas.
- func closeCCAnthropicBlock(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
- if !state.ContentBlockOpen {
- return nil
- }
- idx := state.ContentBlockIndex
- var events []AnthropicStreamEvent
- if state.CurrentBlockType == "tool_use" && !state.CurrentToolHadDelta {
- events = append(events, AnthropicStreamEvent{
- Type: "content_block_delta",
- Index: &idx,
- Delta: &AnthropicDelta{
- Type: "input_json_delta",
- PartialJSON: "{}",
- },
- })
- }
- state.ContentBlockOpen = false
- state.ContentBlockIndex++
- state.CurrentBlockType = ""
- state.CurrentToolName = ""
- state.CurrentToolHadDelta = false
- return append(events, AnthropicStreamEvent{
- Type: "content_block_stop",
- Index: &idx,
- })
- }
- // ccFinishReasonToAnthropicStopReason maps a Chat Completions finish_reason
- // (captured during streaming) to an Anthropic stop_reason for message_delta.
- func ccFinishReasonToAnthropicStopReason(reason string, hasToolCall bool) string {
- switch reason {
- case "length":
- return "max_tokens"
- case "tool_calls":
- return "tool_use"
- case "stop":
- if hasToolCall {
- return "tool_use"
- }
- return "end_turn"
- default:
- if hasToolCall {
- return "tool_use"
- }
- return "end_turn"
- }
- }
|