chatcompletions_anthropic_bridge.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "time"
  8. )
  9. // This file implements a DIRECT bridge between Anthropic Messages and OpenAI
  10. // Chat Completions, skipping the Responses API intermediate representation.
  11. //
  12. // The existing chat-fallback path (forwardAnthropicViaRawChatCompletions) chains
  13. // two Responses-anchored bridges — Anthropic→Responses→ChatCompletions on the
  14. // request side and CC→Responses→Anthropic on the response side — so every
  15. // streaming token runs through two state machines. For force-chat accounts
  16. // (third-party OpenAI-compatible upstreams that only speak /v1/chat/completions)
  17. // the Responses layer is pure overhead: these upstreams never see Responses
  18. // semantics, and the clients reaching them via /v1/messages use standard
  19. // function tools (no custom/tool_search/namespace Codex constructs).
  20. //
  21. // The direct bridge collapses both directions into a single conversion each:
  22. //
  23. // Request: Anthropic Messages → Chat Completions
  24. // Response: CC chunk/response → Anthropic events/response
  25. //
  26. // Helper functions from the Responses bridges (anthropicImageToDataURI,
  27. // extractAnthropicTextFromBlocks, fromResponsesCallID, sanitizeAnthropicToolUseInput,
  28. // parseAnthropicSystemContentParts, isReasoningModel, mapAnthropicEffortToResponses,
  29. // normalizeToolParameters) are reused so the conversion semantics stay identical.
  30. // ---------------------------------------------------------------------------
  31. // Request: AnthropicRequest → ChatCompletionsRequest
  32. // ---------------------------------------------------------------------------
  33. // AnthropicToChatCompletionsRequest converts an Anthropic Messages request
  34. // directly into a Chat Completions request, without transiting the Responses
  35. // API. It is semantically equivalent to composing AnthropicToResponses +
  36. // ResponsesToChatCompletionsRequest but avoids materializing the intermediate
  37. // ResponsesRequest and the extra marshal/unmarshal cycle.
  38. func AnthropicToChatCompletionsRequest(req *AnthropicRequest) (*ChatCompletionsRequest, error) {
  39. if req == nil {
  40. return nil, fmt.Errorf("anthropic request is nil")
  41. }
  42. messages, err := anthropicToChatMessages(req.System, req.Messages)
  43. if err != nil {
  44. return nil, err
  45. }
  46. out := &ChatCompletionsRequest{
  47. Model: req.Model,
  48. Messages: messages,
  49. Stream: req.Stream,
  50. }
  51. // Sampling params: reasoning models (gpt-5.x) reject temperature/top_p.
  52. if !isReasoningModel(req.Model) {
  53. out.Temperature = req.Temperature
  54. out.TopP = req.TopP
  55. }
  56. if req.MaxTokens > 0 {
  57. v := req.MaxTokens
  58. if v < minMaxOutputTokens {
  59. v = minMaxOutputTokens
  60. }
  61. out.MaxCompletionTokens = &v
  62. }
  63. // Tools: Anthropic input_schema is a JSON Schema, directly usable as Chat
  64. // function parameters. Server tools (web_search_*) have no Chat Completions
  65. // equivalent and are dropped (mirrors responsesToolsToChatTools).
  66. if len(req.Tools) > 0 {
  67. tools := anthropicToolsToChatTools(req.Tools)
  68. if len(tools) > 0 {
  69. out.Tools = tools
  70. }
  71. }
  72. // tool_choice is only forwarded when tools survived the conversion
  73. // (upstream rejects tool_choice without tools), and a named choice only when
  74. // it points at a declared tool — mirroring responsesToolChoiceToChatToolChoice,
  75. // chat upstreams 400 on tool_choice referencing an unknown tool.
  76. if len(out.Tools) > 0 && len(req.ToolChoice) > 0 {
  77. declared := make(map[string]bool, len(out.Tools))
  78. for _, tool := range out.Tools {
  79. if tool.Function != nil {
  80. declared[tool.Function.Name] = true
  81. }
  82. }
  83. tc, err := convertAnthropicToolChoiceToChat(req.ToolChoice, declared)
  84. if err != nil {
  85. return nil, fmt.Errorf("convert tool_choice: %w", err)
  86. }
  87. if len(tc) > 0 {
  88. out.ToolChoice = tc
  89. }
  90. }
  91. // Reasoning effort: output_config.effort maps 1:1 (max→xhigh). thinking.type
  92. // itself is ignored (the Responses bridge behaves identically).
  93. effort := "medium"
  94. if req.OutputConfig != nil && req.OutputConfig.Effort != "" {
  95. effort = req.OutputConfig.Effort
  96. }
  97. out.ReasoningEffort = mapAnthropicEffortToResponses(effort)
  98. parallelToolCalls := true
  99. out.ParallelToolCalls = &parallelToolCalls
  100. return out, nil
  101. }
  102. // anthropicToChatMessages converts the Anthropic system field + message list
  103. // into Chat Completions messages. It mirrors convertAnthropicToResponsesInput +
  104. // responsesInputToChatMessages but produces ChatMessage directly.
  105. func anthropicToChatMessages(system json.RawMessage, msgs []AnthropicMessage) ([]ChatMessage, error) {
  106. var messages []ChatMessage
  107. // System prompt → system message. parseAnthropicSystemContentParts handles
  108. // both string and []block forms and filters the billing header.
  109. if len(system) > 0 {
  110. sysParts, err := parseAnthropicSystemContentParts(system)
  111. if err != nil {
  112. return nil, err
  113. }
  114. if len(sysParts) > 0 {
  115. text := joinResponsesContentPartText(sysParts)
  116. if text != "" {
  117. content, _ := json.Marshal(text)
  118. messages = append(messages, ChatMessage{Role: "system", Content: content})
  119. }
  120. }
  121. }
  122. for _, m := range msgs {
  123. converted, err := anthropicMsgToChatMessages(m)
  124. if err != nil {
  125. return nil, err
  126. }
  127. messages = append(messages, converted...)
  128. }
  129. return normalizeChatMessages(messages), nil
  130. }
  131. // anthropicMsgToChatMessages converts one Anthropic message into one or more
  132. // Chat messages. tool_result blocks become standalone "tool" role messages
  133. // (the Chat Completions convention); text/image blocks stay in a user message;
  134. // assistant tool_use blocks become tool_calls on the assistant message.
  135. func anthropicMsgToChatMessages(m AnthropicMessage) ([]ChatMessage, error) {
  136. switch m.Role {
  137. case "assistant":
  138. return anthropicAssistantToChatMessages(m.Content)
  139. default: // "user" and any unknown role
  140. return anthropicUserToChatMessages(m.Content)
  141. }
  142. }
  143. // anthropicUserToChatMessages handles an Anthropic user message. Content may be
  144. // a plain string or an array of blocks. tool_result blocks are extracted into
  145. // standalone "tool" role messages; images inside tool_results are lifted into a
  146. // follow-up user message as image_url parts (the Responses bridge does the same
  147. // — function_call_output only accepts strings, so images must travel separately).
  148. func anthropicUserToChatMessages(raw json.RawMessage) ([]ChatMessage, error) {
  149. // Plain string → single user message.
  150. var s string
  151. if err := json.Unmarshal(raw, &s); err == nil {
  152. content, _ := json.Marshal(s)
  153. return []ChatMessage{{Role: "user", Content: content}}, nil
  154. }
  155. var blocks []AnthropicContentBlock
  156. if err := json.Unmarshal(raw, &blocks); err != nil {
  157. return nil, err
  158. }
  159. var out []ChatMessage
  160. var toolResultImageParts []ChatContentPart
  161. // tool_result → "tool" role messages, text extracted; images deferred.
  162. for _, b := range blocks {
  163. if b.Type != "tool_result" {
  164. continue
  165. }
  166. text, imageParts := convertToolResultOutput(b)
  167. content, _ := json.Marshal(text)
  168. out = append(out, ChatMessage{
  169. Role: "tool",
  170. Content: content,
  171. ToolCallID: b.ToolUseID,
  172. })
  173. for _, ip := range imageParts {
  174. toolResultImageParts = append(toolResultImageParts, ChatContentPart{
  175. Type: "image_url",
  176. ImageURL: &ChatImageURL{URL: ip.ImageURL},
  177. })
  178. }
  179. }
  180. // Remaining text + image blocks → user message. The double-conversion path
  181. // (responsesContentPartsToChatContent) folds text-only content into a single
  182. // string joined with "\n\n" and only uses the parts-array form when an image
  183. // is present — strict chat upstreams reject array content — so the direct
  184. // bridge preserves that folding.
  185. var textParts []string
  186. var parts []ChatContentPart
  187. hasImage := false
  188. for _, b := range blocks {
  189. switch b.Type {
  190. case "text":
  191. if b.Text != "" {
  192. textParts = append(textParts, b.Text)
  193. parts = append(parts, ChatContentPart{Type: "text", Text: b.Text})
  194. }
  195. case "image":
  196. if uri := anthropicImageToDataURI(b.Source); uri != "" {
  197. hasImage = true
  198. parts = append(parts, ChatContentPart{
  199. Type: "image_url",
  200. ImageURL: &ChatImageURL{URL: uri},
  201. })
  202. }
  203. }
  204. }
  205. if len(toolResultImageParts) > 0 {
  206. hasImage = true
  207. parts = append(parts, toolResultImageParts...)
  208. }
  209. if !hasImage {
  210. if len(textParts) > 0 {
  211. content, _ := json.Marshal(strings.Join(textParts, "\n\n"))
  212. out = append(out, ChatMessage{Role: "user", Content: content})
  213. }
  214. return out, nil
  215. }
  216. content, err := json.Marshal(parts)
  217. if err != nil {
  218. return nil, err
  219. }
  220. out = append(out, ChatMessage{Role: "user", Content: content})
  221. return out, nil
  222. }
  223. // anthropicAssistantToChatMessages handles an Anthropic assistant message.
  224. // Text content → assistant message content; tool_use blocks → tool_calls on the
  225. // same assistant message; thinking blocks → reasoning_content, but only on a
  226. // message that carries tool calls (see anthropicThinkingToReasoningContent).
  227. func anthropicAssistantToChatMessages(raw json.RawMessage) ([]ChatMessage, error) {
  228. // Plain string → single assistant message.
  229. var s string
  230. if err := json.Unmarshal(raw, &s); err == nil {
  231. content, _ := json.Marshal(s)
  232. return []ChatMessage{{Role: "assistant", Content: content}}, nil
  233. }
  234. var blocks []AnthropicContentBlock
  235. if err := json.Unmarshal(raw, &blocks); err != nil {
  236. return nil, err
  237. }
  238. msg := ChatMessage{Role: "assistant"}
  239. text := extractAnthropicTextFromBlocks(blocks)
  240. if text != "" {
  241. content, _ := json.Marshal(text)
  242. msg.Content = content
  243. }
  244. for _, b := range blocks {
  245. if b.Type != "tool_use" {
  246. continue
  247. }
  248. args := "{}"
  249. if len(b.Input) > 0 {
  250. args = string(b.Input)
  251. }
  252. msg.ToolCalls = append(msg.ToolCalls, ChatToolCall{
  253. ID: b.ID,
  254. Type: "function",
  255. Function: ChatFunctionCall{
  256. Name: b.Name,
  257. Arguments: args,
  258. },
  259. })
  260. }
  261. msg.ReasoningContent = anthropicThinkingToReasoningContent(blocks, len(msg.ToolCalls) > 0)
  262. return []ChatMessage{msg}, nil
  263. }
  264. // anthropicThinkingToReasoningContent folds thinking blocks back into the
  265. // Chat Completions reasoning_content field.
  266. //
  267. // chatMessageToAnthropicBlocks emits the upstream's reasoning_content as a
  268. // thinking block on the way out, so a multi-turn client echoes it back on the
  269. // next request; dropping it here made the bridge lose exactly what it had just
  270. // produced. DeepSeek's thinking mode requires the reasoning_content that
  271. // produced a tool call to be replayed on that assistant message and answers
  272. // 400 otherwise, which is why buildChatMessagesFromItems already carries
  273. // pendingReasoning onto assistant tool-call messages in the Responses→Chat
  274. // bridge. hasToolCalls keeps the scope identical to that sibling: reasoning
  275. // rides along with tool calls only, never on a plain assistant text turn.
  276. //
  277. // redacted_thinking blocks and signature-only placeholders carry no plaintext
  278. // and contribute nothing. Multiple blocks join with "\n", matching
  279. // extractResponsesReasoningText.
  280. func anthropicThinkingToReasoningContent(blocks []AnthropicContentBlock, hasToolCalls bool) string {
  281. if !hasToolCalls {
  282. return ""
  283. }
  284. var parts []string
  285. for _, b := range blocks {
  286. if b.Type == "thinking" && b.Thinking != "" {
  287. parts = append(parts, b.Thinking)
  288. }
  289. }
  290. return strings.Join(parts, "\n")
  291. }
  292. // anthropicToolsToChatTools maps Anthropic tool definitions to Chat Completions
  293. // function tools. Server-side tools (web_search_*) are dropped — they have no
  294. // Chat Completions equivalent.
  295. func anthropicToolsToChatTools(tools []AnthropicTool) []ChatTool {
  296. var out []ChatTool
  297. for _, t := range tools {
  298. if strings.HasPrefix(t.Type, "web_search") {
  299. continue
  300. }
  301. out = append(out, ChatTool{
  302. Type: "function",
  303. Function: &ChatFunction{
  304. Name: t.Name,
  305. Description: t.Description,
  306. Parameters: normalizeToolParameters(t.InputSchema),
  307. Strict: boolPtr(false),
  308. },
  309. })
  310. }
  311. return out
  312. }
  313. // convertAnthropicToolChoiceToChat maps Anthropic tool_choice to Chat
  314. // Completions tool_choice. A nil result means the choice is dropped: like the
  315. // double-conversion path (responsesToolChoiceToChatToolChoice), a named choice
  316. // pointing at an undeclared tool or an unknown choice type is not forwarded,
  317. // because chat upstreams reject it.
  318. //
  319. // {"type":"auto"} → "auto"
  320. // {"type":"any"} → "required"
  321. // {"type":"none"} → "none"
  322. // {"type":"tool","name":"X"} → {"type":"function","function":{"name":"X"}} (X declared)
  323. func convertAnthropicToolChoiceToChat(raw json.RawMessage, declared map[string]bool) (json.RawMessage, error) {
  324. var tc struct {
  325. Type string `json:"type"`
  326. Name string `json:"name"`
  327. }
  328. if err := json.Unmarshal(raw, &tc); err != nil {
  329. return nil, err
  330. }
  331. switch tc.Type {
  332. case "auto":
  333. return json.Marshal("auto")
  334. case "any":
  335. return json.Marshal("required")
  336. case "none":
  337. return json.Marshal("none")
  338. case "tool":
  339. if tc.Name == "" || !declared[tc.Name] {
  340. return nil, nil
  341. }
  342. return json.Marshal(map[string]any{
  343. "type": "function",
  344. "function": map[string]string{"name": tc.Name},
  345. })
  346. default:
  347. return nil, nil
  348. }
  349. }
  350. // joinResponsesContentPartText concatenates the text of input_text parts. Used
  351. // for the system prompt where parseAnthropicSystemContentParts returns
  352. // ResponsesContentPart values.
  353. func joinResponsesContentPartText(parts []ResponsesContentPart) string {
  354. var texts []string
  355. for _, p := range parts {
  356. if p.Type == "input_text" && p.Text != "" {
  357. texts = append(texts, p.Text)
  358. }
  359. }
  360. return strings.Join(texts, "\n\n")
  361. }
  362. // ---------------------------------------------------------------------------
  363. // Non-streaming response: ChatCompletionsResponse → AnthropicResponse
  364. // ---------------------------------------------------------------------------
  365. // ChatCompletionsResponseToAnthropic converts a Chat Completions response
  366. // directly into an Anthropic Messages response, without materializing a
  367. // ResponsesResponse. It is semantically equivalent to composing
  368. // ChatCompletionsResponseToResponses + ResponsesToAnthropic.
  369. func ChatCompletionsResponseToAnthropic(resp *ChatCompletionsResponse, model string) *AnthropicResponse {
  370. out := &AnthropicResponse{
  371. Type: "message",
  372. Role: "assistant",
  373. Model: model,
  374. }
  375. if resp != nil {
  376. out.ID = resp.ID
  377. if out.Model == "" {
  378. out.Model = resp.Model
  379. }
  380. if len(resp.Choices) > 0 {
  381. choice := resp.Choices[0]
  382. out.Content = chatMessageToAnthropicBlocks(choice.Message)
  383. out.StopReason = AnthropicStopReasonPtr(chatFinishReasonToAnthropicStopReason(choice.FinishReason, out.Content))
  384. // "length" → "max_tokens" is handled by chatFinishReasonToAnthropicStopReason;
  385. // Anthropic conveys max-tokens via stop_reason only, no incomplete_details field.
  386. }
  387. if resp.Usage != nil {
  388. out.Usage = chatUsageToAnthropicUsage(resp.Usage)
  389. }
  390. }
  391. if len(out.Content) == 0 {
  392. out.Content = []AnthropicContentBlock{{Type: "text", Text: ""}}
  393. }
  394. // Empty choices / nil response never enter the choices branch above; the
  395. // double-conversion path still reports a completed turn ("end_turn"), and
  396. // stop_reason null/"" is invalid for completed non-stream responses.
  397. if AnthropicStopReasonString(out.StopReason) == "" {
  398. out.StopReason = AnthropicStopReasonPtr(chatFinishReasonToAnthropicStopReason("", out.Content))
  399. }
  400. // The double-conversion path generates a response id when the upstream
  401. // omits one (ChatCompletionsResponseToResponses); clients treat it as required.
  402. if out.ID == "" {
  403. out.ID = generateResponsesID()
  404. }
  405. return out
  406. }
  407. // chatMessageToAnthropicBlocks converts a Chat Completions message into
  408. // Anthropic content blocks. Reasoning content → thinking block; text content →
  409. // text block; tool_calls → tool_use blocks. Mirrors chatMessageToResponsesOutput
  410. // + the reasoning→thinking mapping in ResponsesToAnthropic.
  411. func chatMessageToAnthropicBlocks(message ChatMessage) []AnthropicContentBlock {
  412. var blocks []AnthropicContentBlock
  413. reasoning := message.reasoningText()
  414. if reasoning != "" {
  415. blocks = append(blocks, AnthropicContentBlock{
  416. Type: "thinking",
  417. Thinking: reasoning,
  418. })
  419. }
  420. text := chatMessageContentText(message.Content)
  421. // DeepSeek reasoning-only fallback: when there is no text and no tool calls,
  422. // surface the reasoning content as visible text so the turn isn't empty.
  423. if text == "" && strings.TrimSpace(reasoning) != "" && len(message.ToolCalls) == 0 {
  424. text = reasoning
  425. }
  426. if text != "" || len(message.ToolCalls) == 0 {
  427. blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: text})
  428. }
  429. for _, toolCall := range message.ToolCalls {
  430. arguments := toolCall.Function.Arguments
  431. if strings.TrimSpace(arguments) == "" {
  432. arguments = "{}"
  433. }
  434. blocks = append(blocks, AnthropicContentBlock{
  435. Type: "tool_use",
  436. ID: fromResponsesCallID(toolCall.ID),
  437. Name: toolCall.Function.Name,
  438. Input: sanitizeAnthropicToolUseInput(toolCall.Function.Name, arguments),
  439. })
  440. }
  441. return blocks
  442. }
  443. // chatFinishReasonToAnthropicStopReason maps Chat Completions finish_reason to
  444. // Anthropic stop_reason.
  445. //
  446. // "length" → "max_tokens"
  447. // "tool_calls" → "tool_use"
  448. // other → "end_turn" (or "tool_use" if tool_use blocks present)
  449. //
  450. // "stop", "content_filter", and unknown reasons all map to a completed response
  451. // in the double-conversion path, which then derives stop_reason from the blocks.
  452. func chatFinishReasonToAnthropicStopReason(reason string, blocks []AnthropicContentBlock) string {
  453. switch reason {
  454. case "length":
  455. return "max_tokens"
  456. case "tool_calls":
  457. return "tool_use"
  458. default:
  459. if containsAnthropicToolUseBlock(blocks) {
  460. return "tool_use"
  461. }
  462. return "end_turn"
  463. }
  464. }
  465. // chatUsageToAnthropicUsage converts Chat Completions token usage to Anthropic
  466. // usage shape. Mirrors ChatUsageToResponsesUsage + anthropicUsageFromResponsesUsage.
  467. func chatUsageToAnthropicUsage(usage *ChatUsage) AnthropicUsage {
  468. if usage == nil {
  469. return AnthropicUsage{}
  470. }
  471. cachedTokens := 0
  472. cacheCreationTokens := 0
  473. if usage.PromptTokensDetails != nil {
  474. cachedTokens = usage.PromptTokensDetails.CachedTokens
  475. // cache_write_tokens and cache_creation_tokens are alternate spellings of
  476. // the same quantity, not additive; the double-conversion path
  477. // (ChatUsageToResponsesUsage) prefers write and falls back to creation.
  478. if usage.PromptTokensDetails.CacheWriteTokens > 0 {
  479. cacheCreationTokens = usage.PromptTokensDetails.CacheWriteTokens
  480. } else {
  481. cacheCreationTokens = usage.PromptTokensDetails.CacheCreationTokens
  482. }
  483. }
  484. inputTokens := usage.PromptTokens - cachedTokens - cacheCreationTokens
  485. if inputTokens < 0 {
  486. inputTokens = 0
  487. }
  488. return AnthropicUsage{
  489. InputTokens: inputTokens,
  490. OutputTokens: usage.CompletionTokens,
  491. CacheReadInputTokens: cachedTokens,
  492. CacheCreationInputTokens: cacheCreationTokens,
  493. }
  494. }
  495. // ---------------------------------------------------------------------------
  496. // Streaming: ChatCompletionsChunk → []AnthropicStreamEvent (stateful converter)
  497. // ---------------------------------------------------------------------------
  498. // ChatCompletionsToAnthropicStreamState tracks state while converting Chat
  499. // Completions SSE chunks directly into Anthropic SSE events. It collapses the
  500. // ChatCompletionsToResponsesStreamState + ResponsesEventToAnthropicState pair
  501. // into one state machine.
  502. type ChatCompletionsToAnthropicStreamState struct {
  503. MessageStartSent bool
  504. MessageStopSent bool
  505. // Current content block lifecycle.
  506. ContentBlockIndex int
  507. ContentBlockOpen bool
  508. CurrentBlockType string // "text" | "thinking" | "tool_use"
  509. CurrentToolName string
  510. CurrentToolHadDelta bool
  511. HasToolCall bool
  512. // Tool calls keyed by the upstream tool_call index. The Anthropic block
  513. // index is assigned when the tool block is announced (content_block_start),
  514. // which is deferred until the tool's name has arrived. Argument fragments
  515. // and the call ID seen before the name are buffered and flushed with the
  516. // announcement; tools whose name never arrives are announced with an empty
  517. // name at finalize so their arguments are not lost.
  518. toolBlockIndex map[int]int
  519. toolAnnounced map[int]bool
  520. toolName map[int]string
  521. pendingToolCallID map[int]string
  522. pendingToolArgs map[int]string
  523. // Reasoning (DeepSeek-style): reasoning_content streamed before content.
  524. // No separate reasoning block index — it uses ContentBlockIndex like the
  525. // Responses bridge's ReasoningIndex, but since blocks are sequential we
  526. // reuse the single ContentBlockIndex counter.
  527. FinishReason string
  528. InputTokens int
  529. OutputTokens int
  530. CacheReadInputTokens int
  531. CacheCreationInputTokens int
  532. ResponseID string
  533. Model string
  534. Created int64
  535. }
  536. // NewChatCompletionsToAnthropicStreamState returns an initialized stream state.
  537. func NewChatCompletionsToAnthropicStreamState(model string) *ChatCompletionsToAnthropicStreamState {
  538. return &ChatCompletionsToAnthropicStreamState{
  539. ResponseID: generateResponsesID(),
  540. Model: model,
  541. Created: time.Now().Unix(),
  542. toolBlockIndex: make(map[int]int),
  543. toolAnnounced: make(map[int]bool),
  544. toolName: make(map[int]string),
  545. pendingToolCallID: make(map[int]string),
  546. pendingToolArgs: make(map[int]string),
  547. }
  548. }
  549. // ChatCompletionsChunkToAnthropicEvents converts one Chat Completions stream
  550. // chunk into zero or more Anthropic stream events, updating state as it goes.
  551. func ChatCompletionsChunkToAnthropicEvents(
  552. chunk *ChatCompletionsChunk,
  553. state *ChatCompletionsToAnthropicStreamState,
  554. ) []AnthropicStreamEvent {
  555. if chunk == nil || state == nil {
  556. return nil
  557. }
  558. if chunk.ID != "" {
  559. state.ResponseID = chunk.ID
  560. }
  561. if state.Model == "" && chunk.Model != "" {
  562. state.Model = chunk.Model
  563. }
  564. // Usage in a streaming chunk (include_usage) arrives in its own chunk,
  565. // often with empty choices. Capture it for the finalize message_delta.
  566. if chunk.Usage != nil {
  567. u := chatUsageToAnthropicUsage(chunk.Usage)
  568. state.InputTokens = u.InputTokens
  569. state.OutputTokens = u.OutputTokens
  570. state.CacheReadInputTokens = u.CacheReadInputTokens
  571. state.CacheCreationInputTokens = u.CacheCreationInputTokens
  572. }
  573. var events []AnthropicStreamEvent
  574. events = append(events, ensureCCAnthropicMessageStart(state)...)
  575. for _, choice := range chunk.Choices {
  576. // Reasoning content → thinking block.
  577. reasoning := choice.Delta.reasoningText()
  578. if reasoning != nil && *reasoning != "" {
  579. events = append(events, ensureCCAnthropicThinkingBlock(state)...)
  580. events = append(events, ccAnthropicDelta(state, &AnthropicDelta{
  581. Type: "thinking_delta",
  582. Thinking: *reasoning,
  583. })...)
  584. }
  585. // Text content → text block (closes any open thinking block first).
  586. if choice.Delta.Content != nil && *choice.Delta.Content != "" {
  587. events = append(events, closeCCAnthropicBlockIfOpen(state, "thinking")...)
  588. events = append(events, ensureCCAnthropicTextBlock(state)...)
  589. events = append(events, ccAnthropicDelta(state, &AnthropicDelta{
  590. Type: "text_delta",
  591. Text: *choice.Delta.Content,
  592. })...)
  593. }
  594. // Tool calls → tool_use blocks.
  595. for _, toolCall := range choice.Delta.ToolCalls {
  596. events = append(events, closeCCAnthropicBlockIfOpen(state, "thinking")...)
  597. events = append(events, handleCCAnthropicToolCall(state, &toolCall)...)
  598. }
  599. if choice.FinishReason != nil && *choice.FinishReason != "" {
  600. state.FinishReason = *choice.FinishReason
  601. }
  602. }
  603. return events
  604. }
  605. // FinalizeChatCompletionsAnthropicStream emits terminal Anthropic events
  606. // (close open blocks + message_delta + message_stop) when the stream ends.
  607. func FinalizeChatCompletionsAnthropicStream(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
  608. if state == nil || state.MessageStopSent {
  609. return nil
  610. }
  611. var events []AnthropicStreamEvent
  612. if !state.MessageStartSent {
  613. events = append(events, ensureCCAnthropicMessageStart(state)...)
  614. }
  615. // Announce tools whose name never arrived so their buffered arguments are
  616. // not silently dropped. The double-conversion path announced these
  617. // immediately with an empty name; the deferred announcement keeps that data
  618. // preservation while still delivering correct names when they do arrive.
  619. if len(state.pendingToolCallID) > 0 {
  620. idxs := make([]int, 0, len(state.pendingToolCallID))
  621. for idx := range state.pendingToolCallID {
  622. idxs = append(idxs, idx)
  623. }
  624. sort.Ints(idxs)
  625. for _, idx := range idxs {
  626. callID := state.pendingToolCallID[idx]
  627. events = append(events, closeCCAnthropicBlock(state)...)
  628. events = append(events, announceCCAnthropicToolBlock(state, idx, callID, "")...)
  629. }
  630. }
  631. events = append(events, closeCCAnthropicBlock(state)...)
  632. stopReason := ccFinishReasonToAnthropicStopReason(state.FinishReason, state.HasToolCall)
  633. events = append(events,
  634. AnthropicStreamEvent{
  635. Type: "message_delta",
  636. Delta: &AnthropicDelta{
  637. StopReason: stopReason,
  638. },
  639. Usage: &AnthropicUsage{
  640. InputTokens: state.InputTokens,
  641. OutputTokens: state.OutputTokens,
  642. CacheReadInputTokens: state.CacheReadInputTokens,
  643. CacheCreationInputTokens: state.CacheCreationInputTokens,
  644. },
  645. },
  646. AnthropicStreamEvent{Type: "message_stop"},
  647. )
  648. state.MessageStopSent = true
  649. return events
  650. }
  651. // ensureCCAnthropicMessageStart emits message_start on the first event.
  652. func ensureCCAnthropicMessageStart(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
  653. if state.MessageStartSent {
  654. return nil
  655. }
  656. state.MessageStartSent = true
  657. return []AnthropicStreamEvent{{
  658. Type: "message_start",
  659. Message: &AnthropicResponse{
  660. ID: state.ResponseID,
  661. Type: "message",
  662. Role: "assistant",
  663. Content: []AnthropicContentBlock{},
  664. Model: state.Model,
  665. StopReason: nil, // JSON null; never ""
  666. Usage: AnthropicUsage{InputTokens: 0, OutputTokens: 0},
  667. },
  668. }}
  669. }
  670. // ensureCCAnthropicThinkingBlock opens a thinking block if none is open.
  671. func ensureCCAnthropicThinkingBlock(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
  672. if state.ContentBlockOpen && state.CurrentBlockType == "thinking" {
  673. return nil
  674. }
  675. events := closeCCAnthropicBlock(state)
  676. idx := state.ContentBlockIndex
  677. state.ContentBlockOpen = true
  678. state.CurrentBlockType = "thinking"
  679. events = append(events, AnthropicStreamEvent{
  680. Type: "content_block_start",
  681. Index: &idx,
  682. ContentBlock: &AnthropicContentBlock{
  683. Type: "thinking",
  684. Thinking: "",
  685. },
  686. })
  687. return events
  688. }
  689. // ensureCCAnthropicTextBlock opens a text block if none is open.
  690. func ensureCCAnthropicTextBlock(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
  691. if state.ContentBlockOpen && state.CurrentBlockType == "text" {
  692. return nil
  693. }
  694. events := closeCCAnthropicBlock(state)
  695. idx := state.ContentBlockIndex
  696. state.ContentBlockOpen = true
  697. state.CurrentBlockType = "text"
  698. events = append(events, AnthropicStreamEvent{
  699. Type: "content_block_start",
  700. Index: &idx,
  701. ContentBlock: &AnthropicContentBlock{
  702. Type: "text",
  703. Text: "",
  704. },
  705. })
  706. return events
  707. }
  708. // handleCCAnthropicToolCall processes one upstream tool_call delta. The
  709. // content_block_start for a tool is deferred until its name has arrived (some
  710. // upstreams stream id/arguments before the name); argument fragments seen
  711. // before the announcement are buffered and flushed with it, later fragments
  712. // stream as input_json_delta on the tool's block.
  713. func handleCCAnthropicToolCall(state *ChatCompletionsToAnthropicStreamState, toolCall *ChatToolCall) []AnthropicStreamEvent {
  714. idx := 0
  715. if toolCall.Index != nil {
  716. idx = *toolCall.Index
  717. }
  718. var events []AnthropicStreamEvent
  719. if _, seen := state.toolAnnounced[idx]; !seen {
  720. // New tool call: it ends whatever block is currently streaming.
  721. events = append(events, closeCCAnthropicBlock(state)...)
  722. state.HasToolCall = true
  723. callID := toolCall.ID
  724. if callID == "" {
  725. callID = generateItemID()
  726. }
  727. if name := toolCall.Function.Name; name != "" {
  728. events = append(events, announceCCAnthropicToolBlock(state, idx, callID, name)...)
  729. } else {
  730. state.toolAnnounced[idx] = false
  731. state.pendingToolCallID[idx] = callID
  732. }
  733. } else if !state.toolAnnounced[idx] && toolCall.Function.Name != "" {
  734. // Deferred announcement: the name has arrived.
  735. callID := state.pendingToolCallID[idx]
  736. if toolCall.ID != "" {
  737. callID = toolCall.ID
  738. }
  739. events = append(events, closeCCAnthropicBlock(state)...)
  740. events = append(events, announceCCAnthropicToolBlock(state, idx, callID, toolCall.Function.Name)...)
  741. }
  742. // Argument fragment → input_json_delta on the tool's block once announced,
  743. // buffered until the deferred announcement otherwise.
  744. if toolCall.Function.Arguments != "" {
  745. if state.toolAnnounced[idx] {
  746. blockIdx := state.toolBlockIndex[idx]
  747. if state.ContentBlockOpen && blockIdx == state.ContentBlockIndex {
  748. state.CurrentToolHadDelta = true
  749. }
  750. events = append(events, AnthropicStreamEvent{
  751. Type: "content_block_delta",
  752. Index: &blockIdx,
  753. Delta: &AnthropicDelta{
  754. Type: "input_json_delta",
  755. PartialJSON: toolCall.Function.Arguments,
  756. },
  757. })
  758. } else {
  759. state.pendingToolArgs[idx] += toolCall.Function.Arguments
  760. }
  761. }
  762. return events
  763. }
  764. // announceCCAnthropicToolBlock assigns the next Anthropic block index to the
  765. // tool, emits its content_block_start, and flushes any argument fragments
  766. // buffered while the announcement was deferred.
  767. func announceCCAnthropicToolBlock(state *ChatCompletionsToAnthropicStreamState, idx int, callID, name string) []AnthropicStreamEvent {
  768. blockIdx := state.ContentBlockIndex
  769. state.toolBlockIndex[idx] = blockIdx
  770. state.toolAnnounced[idx] = true
  771. state.toolName[idx] = name
  772. state.CurrentToolName = name
  773. state.CurrentToolHadDelta = false
  774. state.ContentBlockOpen = true
  775. state.CurrentBlockType = "tool_use"
  776. delete(state.pendingToolCallID, idx)
  777. events := []AnthropicStreamEvent{{
  778. Type: "content_block_start",
  779. Index: &blockIdx,
  780. ContentBlock: &AnthropicContentBlock{
  781. Type: "tool_use",
  782. ID: fromResponsesCallID(callID),
  783. Name: name,
  784. Input: json.RawMessage("{}"),
  785. },
  786. }}
  787. if pending := state.pendingToolArgs[idx]; pending != "" {
  788. delete(state.pendingToolArgs, idx)
  789. state.CurrentToolHadDelta = true
  790. events = append(events, AnthropicStreamEvent{
  791. Type: "content_block_delta",
  792. Index: &blockIdx,
  793. Delta: &AnthropicDelta{
  794. Type: "input_json_delta",
  795. PartialJSON: pending,
  796. },
  797. })
  798. }
  799. return events
  800. }
  801. // ccAnthropicDelta emits a content_block_delta on the current block.
  802. func ccAnthropicDelta(state *ChatCompletionsToAnthropicStreamState, delta *AnthropicDelta) []AnthropicStreamEvent {
  803. if !state.ContentBlockOpen {
  804. return nil
  805. }
  806. idx := state.ContentBlockIndex
  807. return []AnthropicStreamEvent{{
  808. Type: "content_block_delta",
  809. Index: &idx,
  810. Delta: delta,
  811. }}
  812. }
  813. // closeCCAnthropicBlockIfOpen closes the current block only if it matches the
  814. // given type (used to close a thinking block before opening text/tool).
  815. func closeCCAnthropicBlockIfOpen(state *ChatCompletionsToAnthropicStreamState, blockType string) []AnthropicStreamEvent {
  816. if !state.ContentBlockOpen || state.CurrentBlockType != blockType {
  817. return nil
  818. }
  819. return closeCCAnthropicBlock(state)
  820. }
  821. // closeCCAnthropicBlock closes the currently open content block. A tool_use
  822. // block that streamed no argument delta gets a final input_json_delta "{}"
  823. // first — the double-conversion path normalizes empty tool arguments to "{}",
  824. // and some clients assemble tool input exclusively from deltas.
  825. func closeCCAnthropicBlock(state *ChatCompletionsToAnthropicStreamState) []AnthropicStreamEvent {
  826. if !state.ContentBlockOpen {
  827. return nil
  828. }
  829. idx := state.ContentBlockIndex
  830. var events []AnthropicStreamEvent
  831. if state.CurrentBlockType == "tool_use" && !state.CurrentToolHadDelta {
  832. events = append(events, AnthropicStreamEvent{
  833. Type: "content_block_delta",
  834. Index: &idx,
  835. Delta: &AnthropicDelta{
  836. Type: "input_json_delta",
  837. PartialJSON: "{}",
  838. },
  839. })
  840. }
  841. state.ContentBlockOpen = false
  842. state.ContentBlockIndex++
  843. state.CurrentBlockType = ""
  844. state.CurrentToolName = ""
  845. state.CurrentToolHadDelta = false
  846. return append(events, AnthropicStreamEvent{
  847. Type: "content_block_stop",
  848. Index: &idx,
  849. })
  850. }
  851. // ccFinishReasonToAnthropicStopReason maps a Chat Completions finish_reason
  852. // (captured during streaming) to an Anthropic stop_reason for message_delta.
  853. func ccFinishReasonToAnthropicStopReason(reason string, hasToolCall bool) string {
  854. switch reason {
  855. case "length":
  856. return "max_tokens"
  857. case "tool_calls":
  858. return "tool_use"
  859. case "stop":
  860. if hasToolCall {
  861. return "tool_use"
  862. }
  863. return "end_turn"
  864. default:
  865. if hasToolCall {
  866. return "tool_use"
  867. }
  868. return "end_turn"
  869. }
  870. }