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 are dropped (Chat Completions has no // inbound thinking field, matching anthropicAssistantToResponses). 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, }, }) } return []ChatMessage{msg}, nil } // 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" } }