responses_to_anthropic_request.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. )
  7. // ResponsesToAnthropicRequest converts a Responses API request into an
  8. // Anthropic Messages request. This is the reverse of AnthropicToResponses and
  9. // enables Anthropic platform groups to accept OpenAI Responses API requests
  10. // by converting them to the native /v1/messages format before forwarding upstream.
  11. func ResponsesToAnthropicRequest(req *ResponsesRequest) (*AnthropicRequest, error) {
  12. system, messages, err := convertResponsesInputToAnthropic(req.Instructions, req.Input)
  13. if err != nil {
  14. return nil, err
  15. }
  16. out := &AnthropicRequest{
  17. Model: req.Model,
  18. Messages: messages,
  19. Temperature: req.Temperature,
  20. TopP: req.TopP,
  21. Stream: req.Stream,
  22. }
  23. if len(system) > 0 {
  24. out.System = system
  25. }
  26. // max_output_tokens → max_tokens
  27. if req.MaxOutputTokens != nil && *req.MaxOutputTokens > 0 {
  28. out.MaxTokens = *req.MaxOutputTokens
  29. }
  30. if out.MaxTokens == 0 {
  31. // Anthropic requires max_tokens; default to a sensible value.
  32. out.MaxTokens = 8192
  33. }
  34. // Convert tools
  35. if len(req.Tools) > 0 {
  36. out.Tools = convertResponsesToAnthropicTools(req.Tools)
  37. }
  38. // Convert tool_choice (reverse of convertAnthropicToolChoiceToResponses)
  39. if len(req.ToolChoice) > 0 {
  40. tc, err := convertResponsesToAnthropicToolChoice(req.ToolChoice)
  41. if err != nil {
  42. return nil, fmt.Errorf("convert tool_choice: %w", err)
  43. }
  44. out.ToolChoice = tc
  45. }
  46. // reasoning.effort → output_config.effort + thinking
  47. if req.Reasoning != nil && req.Reasoning.Effort != "" {
  48. effort := mapResponsesEffortToAnthropic(req.Reasoning.Effort)
  49. out.OutputConfig = &AnthropicOutputConfig{Effort: effort}
  50. // Enable thinking for non-low efforts
  51. if effort != "low" {
  52. out.Thinking = &AnthropicThinking{
  53. Type: "enabled",
  54. BudgetTokens: defaultThinkingBudget(effort),
  55. }
  56. }
  57. }
  58. return out, nil
  59. }
  60. // defaultThinkingBudget returns a sensible thinking budget based on effort level.
  61. func defaultThinkingBudget(effort string) int {
  62. switch effort {
  63. case "low":
  64. return 1024
  65. case "medium":
  66. return 4096
  67. case "high":
  68. return 10240
  69. case "max":
  70. return 32768
  71. default:
  72. return 10240
  73. }
  74. }
  75. // mapResponsesEffortToAnthropic converts OpenAI Responses reasoning effort to
  76. // Anthropic effort levels. Reverse of mapAnthropicEffortToResponses.
  77. //
  78. // low → low
  79. // medium → medium
  80. // high → high
  81. // xhigh → max
  82. func mapResponsesEffortToAnthropic(effort string) string {
  83. if effort == "xhigh" {
  84. return "max"
  85. }
  86. return effort // low→low, medium→medium, high→high, unknown→passthrough
  87. }
  88. // convertResponsesInputToAnthropic extracts system prompt and messages from
  89. // a Responses API instructions + input array. Returns the system as raw JSON
  90. // (for Anthropic's polymorphic system field) and a list of Anthropic messages.
  91. func convertResponsesInputToAnthropic(instructions string, inputRaw json.RawMessage) (json.RawMessage, []AnthropicMessage, error) {
  92. var systemParts []string
  93. if strings.TrimSpace(instructions) != "" {
  94. systemParts = append(systemParts, strings.TrimSpace(instructions))
  95. }
  96. // Try as plain string input.
  97. var inputStr string
  98. if err := json.Unmarshal(inputRaw, &inputStr); err == nil {
  99. content, _ := json.Marshal(inputStr)
  100. var system json.RawMessage
  101. if len(systemParts) > 0 {
  102. system, _ = json.Marshal(strings.Join(systemParts, "\n\n"))
  103. }
  104. return system, []AnthropicMessage{{Role: "user", Content: content}}, nil
  105. }
  106. var items []ResponsesInputItem
  107. if err := json.Unmarshal(inputRaw, &items); err != nil {
  108. return nil, nil, fmt.Errorf("parse responses input: %w", err)
  109. }
  110. var messages []AnthropicMessage
  111. for _, item := range items {
  112. switch {
  113. case item.Role == "system" || item.Role == "developer":
  114. text := extractTextFromContent(item.Content)
  115. if text != "" {
  116. systemParts = append(systemParts, text)
  117. }
  118. case item.Type == "function_call":
  119. // function_call → assistant message with tool_use block
  120. input := json.RawMessage("{}")
  121. if item.Arguments != "" {
  122. input = json.RawMessage(item.Arguments)
  123. }
  124. block := AnthropicContentBlock{
  125. Type: "tool_use",
  126. ID: fromResponsesCallIDToAnthropic(item.CallID),
  127. Name: item.Name,
  128. Input: input,
  129. }
  130. blockJSON, _ := json.Marshal([]AnthropicContentBlock{block})
  131. messages = append(messages, AnthropicMessage{
  132. Role: "assistant",
  133. Content: blockJSON,
  134. })
  135. case item.Type == "function_call_output":
  136. // function_call_output → user message with tool_result block
  137. contentJSON := responsesFunctionOutputToAnthropicContent(item)
  138. block := AnthropicContentBlock{
  139. Type: "tool_result",
  140. ToolUseID: fromResponsesCallIDToAnthropic(item.CallID),
  141. Content: contentJSON,
  142. }
  143. blockJSON, _ := json.Marshal([]AnthropicContentBlock{block})
  144. messages = append(messages, AnthropicMessage{
  145. Role: "user",
  146. Content: blockJSON,
  147. })
  148. case item.Type == "reasoning":
  149. // Anthropic 无法摄入 OpenAI 的 reasoning:encrypted_content 是不透明的,
  150. // 而 thinking 块的重放需要 Anthropic 自己签发的 signature,无法伪造。
  151. // Codex 常见形态(只带 summary + encrypted_content)本来就会被丢弃,
  152. // 这里让带 content 数组的形态保持同样行为——否则 reasoning_text 块会被
  153. // 原样塞进 Anthropic 请求体,上游直接回 400。
  154. case item.Role == "user":
  155. content, err := convertResponsesUserToAnthropicContent(item.Content)
  156. if err != nil {
  157. return nil, nil, err
  158. }
  159. // 内容里只有网关不认识的分片时,sanitize 会得到空串。Anthropic 拒收
  160. // 空内容消息("all messages must have non-empty content"),整条丢掉
  161. // 比发一条必然 400 的消息更可用。
  162. if anthropicContentIsEmpty(content) {
  163. continue
  164. }
  165. messages = append(messages, AnthropicMessage{
  166. Role: "user",
  167. Content: content,
  168. })
  169. case item.Role == "assistant":
  170. content, err := convertResponsesAssistantToAnthropicContent(item.Content)
  171. if err != nil {
  172. return nil, nil, err
  173. }
  174. // 同上:分片全不认识时会退化成单个空 text 块,而 Anthropic 拒收
  175. // 空文本块("text content blocks must contain non-whitespace text")。
  176. if anthropicContentIsEmpty(content) || anthropicContentIsOnlyBlankText(content) {
  177. continue
  178. }
  179. messages = append(messages, AnthropicMessage{
  180. Role: "assistant",
  181. Content: content,
  182. })
  183. default:
  184. // 未知 role/type —— 尽量当作 user 消息保留其中的文本/图片。
  185. // 必须走与真实 user 消息同一套白名单转换:直接透传 item.Content 会把
  186. // Responses 专有的分片类型(reasoning_text、web_search_call 的载荷等)
  187. // 原样发给 Anthropic,上游只会回 400 把整轮打挂。
  188. if item.Content == nil {
  189. continue
  190. }
  191. content, err := convertResponsesUserToAnthropicContent(item.Content)
  192. if err != nil {
  193. return nil, nil, err
  194. }
  195. if anthropicContentIsEmpty(content) {
  196. continue
  197. }
  198. messages = append(messages, AnthropicMessage{
  199. Role: "user",
  200. Content: content,
  201. })
  202. }
  203. }
  204. // Repair tool_use/tool_result pairing, then merge consecutive same-role
  205. // messages (Anthropic requires alternating roles). The first merge groups
  206. // parallel calls (and their results) so the pairing pass sees them together;
  207. // the pairing pass may re-split a user turn (e.g. when an injected message
  208. // sat between a call and its output), so a second merge restores alternation.
  209. messages = mergeConsecutiveMessages(messages)
  210. messages = normalizeAnthropicToolPairing(messages)
  211. messages = mergeConsecutiveMessages(messages)
  212. var system json.RawMessage
  213. if len(systemParts) > 0 {
  214. system, _ = json.Marshal(strings.Join(systemParts, "\n\n"))
  215. }
  216. return system, messages, nil
  217. }
  218. func responsesFunctionOutputToAnthropicContent(item ResponsesInputItem) json.RawMessage {
  219. if len(item.outputRaw) == 0 {
  220. output := item.Output
  221. if output == "" {
  222. output = "(empty)"
  223. }
  224. content, _ := json.Marshal(output)
  225. return content
  226. }
  227. var parts []ResponsesContentPart
  228. if err := json.Unmarshal(item.outputRaw, &parts); err == nil {
  229. blocks := make([]AnthropicContentBlock, 0, len(parts))
  230. for _, part := range parts {
  231. switch part.Type {
  232. case "input_text", "output_text", "text":
  233. if part.Text != "" {
  234. blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: part.Text})
  235. }
  236. case "input_image":
  237. if source := dataURIToAnthropicImageSource(part.ImageURL); source != nil {
  238. blocks = append(blocks, AnthropicContentBlock{Type: "image", Source: source})
  239. }
  240. }
  241. }
  242. if len(blocks) > 0 {
  243. content, _ := json.Marshal(blocks)
  244. return content
  245. }
  246. if len(parts) == 0 {
  247. content, _ := json.Marshal("(empty)")
  248. return content
  249. }
  250. }
  251. content, _ := json.Marshal(item.Output)
  252. return content
  253. }
  254. // normalizeAnthropicToolPairing rebuilds the message sequence so it satisfies
  255. // Anthropic's tool_use/tool_result invariants, which the naive item-by-item
  256. // conversion violates whenever the Responses history interleaves anything
  257. // between a function_call and its function_call_output:
  258. //
  259. // - every tool_result block must have a matching tool_use in the immediately
  260. // preceding assistant message ("tool_result ... must have a corresponding
  261. // tool_use block in the previous message");
  262. // - every tool_use block must be answered by a tool_result in the immediately
  263. // following user message (Anthropic rejects unanswered tool_use ids);
  264. // - user/assistant turns must alternate.
  265. //
  266. // codex (Responses, store:false) re-sends the whole history each turn and
  267. // frequently injects items between a call and its output — a developer/approval
  268. // notice, or a sibling parallel call whose output never arrived. The unrepaired
  269. // converter emits each function_call as its own assistant message and each
  270. // output as its own user message, so any such interleaving breaks
  271. // tool_use↔tool_result adjacency and yields an upstream 400.
  272. //
  273. // The repair indexes every tool_result by its tool_use id, then for each
  274. // assistant message carrying tool_use blocks keeps only the answered ones
  275. // (dropping unanswered/dangling calls — and the assistant message entirely if it
  276. // has no other content) and emits the matching tool_result blocks, in call
  277. // order, as the very next user message. Standalone tool_result blocks are
  278. // dropped from their original position (re-emitted adjacent to their call);
  279. // orphan tool_results with no announcing tool_use are dropped. Non-tool content
  280. // passes through in place. This mirrors normalizeChatMessages on the
  281. // Responses→Chat path.
  282. func normalizeAnthropicToolPairing(messages []AnthropicMessage) []AnthropicMessage {
  283. // Index every tool_result block by its tool_use id (last wins on dup).
  284. results := make(map[string]AnthropicContentBlock)
  285. for _, m := range messages {
  286. if m.Role != "user" {
  287. continue
  288. }
  289. for _, b := range parseContentBlocks(m.Content) {
  290. if b.Type == "tool_result" && b.ToolUseID != "" {
  291. results[b.ToolUseID] = b
  292. }
  293. }
  294. }
  295. out := make([]AnthropicMessage, 0, len(messages))
  296. for _, m := range messages {
  297. blocks := parseContentBlocks(m.Content)
  298. switch m.Role {
  299. case "assistant":
  300. var toolUses, others []AnthropicContentBlock
  301. for _, b := range blocks {
  302. if b.Type == "tool_use" {
  303. toolUses = append(toolUses, b)
  304. } else {
  305. others = append(others, b)
  306. }
  307. }
  308. if len(toolUses) == 0 {
  309. out = append(out, m)
  310. continue
  311. }
  312. kept := make([]AnthropicContentBlock, 0, len(toolUses))
  313. for _, tu := range toolUses {
  314. if _, ok := results[tu.ID]; ok {
  315. kept = append(kept, tu)
  316. }
  317. }
  318. if len(kept) == 0 {
  319. // No answered calls: keep any non-tool content, else drop.
  320. if len(others) > 0 {
  321. out = append(out, anthropicMessageFromBlocks("assistant", others))
  322. }
  323. continue
  324. }
  325. asstBlocks := make([]AnthropicContentBlock, 0, len(others)+len(kept))
  326. asstBlocks = append(asstBlocks, others...)
  327. asstBlocks = append(asstBlocks, kept...)
  328. out = append(out, anthropicMessageFromBlocks("assistant", asstBlocks))
  329. resBlocks := make([]AnthropicContentBlock, 0, len(kept))
  330. for _, tu := range kept {
  331. resBlocks = append(resBlocks, results[tu.ID])
  332. }
  333. out = append(out, anthropicMessageFromBlocks("user", resBlocks))
  334. case "user":
  335. var nonResult []AnthropicContentBlock
  336. hasResult := false
  337. for _, b := range blocks {
  338. if b.Type == "tool_result" {
  339. hasResult = true
  340. continue
  341. }
  342. nonResult = append(nonResult, b)
  343. }
  344. if !hasResult {
  345. out = append(out, m)
  346. continue
  347. }
  348. // The tool_result blocks are re-emitted next to their call; keep any
  349. // other content of this user turn in place, drop it if there is none.
  350. if len(nonResult) > 0 {
  351. out = append(out, anthropicMessageFromBlocks("user", nonResult))
  352. }
  353. default:
  354. out = append(out, m)
  355. }
  356. }
  357. return out
  358. }
  359. // anthropicMessageFromBlocks builds an AnthropicMessage whose content is the
  360. // marshaled block array.
  361. func anthropicMessageFromBlocks(role string, blocks []AnthropicContentBlock) AnthropicMessage {
  362. content, _ := json.Marshal(blocks)
  363. return AnthropicMessage{Role: role, Content: content}
  364. }
  365. // extractTextFromContent extracts text from a content field that may be a
  366. // plain string or an array of content parts.
  367. func extractTextFromContent(raw json.RawMessage) string {
  368. if len(raw) == 0 {
  369. return ""
  370. }
  371. var s string
  372. if err := json.Unmarshal(raw, &s); err == nil {
  373. return s
  374. }
  375. var parts []ResponsesContentPart
  376. if err := json.Unmarshal(raw, &parts); err == nil {
  377. var texts []string
  378. for _, p := range parts {
  379. if (p.Type == "input_text" || p.Type == "output_text" || p.Type == "text") && p.Text != "" {
  380. texts = append(texts, p.Text)
  381. }
  382. }
  383. return strings.Join(texts, "\n\n")
  384. }
  385. return ""
  386. }
  387. // convertResponsesUserToAnthropicContent converts a Responses user message
  388. // content field into Anthropic content blocks JSON.
  389. // anthropicContentIsEmpty 判断转换结果是否为"空内容"。
  390. // convertResponsesUserToAnthropicContent 在没有任何可识别分片时返回 JSON 空串,
  391. // 而 Anthropic 拒收空内容消息。
  392. func anthropicContentIsEmpty(content json.RawMessage) bool {
  393. trimmed := strings.TrimSpace(string(content))
  394. switch trimmed {
  395. case "", "null", `""`, "[]":
  396. return true
  397. }
  398. return false
  399. }
  400. // anthropicContentIsOnlyBlankText 判断内容是否只由空白 text 块组成。
  401. func anthropicContentIsOnlyBlankText(content json.RawMessage) bool {
  402. blocks := parseContentBlocks(content)
  403. if len(blocks) == 0 {
  404. return false
  405. }
  406. for _, b := range blocks {
  407. if b.Type != "text" || strings.TrimSpace(b.Text) != "" {
  408. return false
  409. }
  410. }
  411. return true
  412. }
  413. func convertResponsesUserToAnthropicContent(raw json.RawMessage) (json.RawMessage, error) {
  414. if len(raw) == 0 {
  415. return json.Marshal("") // empty string content
  416. }
  417. // Try plain string.
  418. var s string
  419. if err := json.Unmarshal(raw, &s); err == nil {
  420. return json.Marshal(s)
  421. }
  422. // Array of content parts → Anthropic content blocks.
  423. var parts []ResponsesContentPart
  424. if err := json.Unmarshal(raw, &parts); err != nil {
  425. // Pass through as-is if we can't parse
  426. return raw, nil
  427. }
  428. var blocks []AnthropicContentBlock
  429. for _, p := range parts {
  430. switch p.Type {
  431. case "input_text", "text":
  432. if p.Text != "" {
  433. blocks = append(blocks, AnthropicContentBlock{
  434. Type: "text",
  435. Text: p.Text,
  436. })
  437. }
  438. case "input_image":
  439. src := dataURIToAnthropicImageSource(p.ImageURL)
  440. if src != nil {
  441. blocks = append(blocks, AnthropicContentBlock{
  442. Type: "image",
  443. Source: src,
  444. })
  445. }
  446. }
  447. }
  448. if len(blocks) == 0 {
  449. return json.Marshal("")
  450. }
  451. return json.Marshal(blocks)
  452. }
  453. // convertResponsesAssistantToAnthropicContent converts a Responses assistant
  454. // message content field into Anthropic content blocks JSON.
  455. func convertResponsesAssistantToAnthropicContent(raw json.RawMessage) (json.RawMessage, error) {
  456. if len(raw) == 0 {
  457. return json.Marshal([]AnthropicContentBlock{{Type: "text", Text: ""}})
  458. }
  459. // Try plain string.
  460. var s string
  461. if err := json.Unmarshal(raw, &s); err == nil {
  462. return json.Marshal([]AnthropicContentBlock{{Type: "text", Text: s}})
  463. }
  464. // Array of content parts → Anthropic content blocks.
  465. var parts []ResponsesContentPart
  466. if err := json.Unmarshal(raw, &parts); err != nil {
  467. return raw, nil
  468. }
  469. var blocks []AnthropicContentBlock
  470. for _, p := range parts {
  471. switch p.Type {
  472. case "output_text", "text":
  473. if p.Text != "" {
  474. blocks = append(blocks, AnthropicContentBlock{
  475. Type: "text",
  476. Text: p.Text,
  477. })
  478. }
  479. }
  480. }
  481. if len(blocks) == 0 {
  482. blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: ""})
  483. }
  484. return json.Marshal(blocks)
  485. }
  486. // fromResponsesCallIDToAnthropic converts an OpenAI function call ID back to
  487. // Anthropic format. Reverses toResponsesCallID.
  488. func fromResponsesCallIDToAnthropic(id string) string {
  489. // If it has our "fc_" prefix wrapping a known Anthropic prefix, strip it
  490. if after, ok := strings.CutPrefix(id, "fc_"); ok {
  491. if strings.HasPrefix(after, "toolu_") || strings.HasPrefix(after, "call_") {
  492. return after
  493. }
  494. }
  495. // Generate a synthetic Anthropic tool ID
  496. if !strings.HasPrefix(id, "toolu_") && !strings.HasPrefix(id, "call_") {
  497. return "toolu_" + id
  498. }
  499. return id
  500. }
  501. // dataURIToAnthropicImageSource parses a data URI into an AnthropicImageSource.
  502. func dataURIToAnthropicImageSource(dataURI string) *AnthropicImageSource {
  503. if !strings.HasPrefix(dataURI, "data:") {
  504. return nil
  505. }
  506. // Format: data:<media_type>;base64,<data>
  507. rest := strings.TrimPrefix(dataURI, "data:")
  508. semicolonIdx := strings.Index(rest, ";")
  509. if semicolonIdx < 0 {
  510. return nil
  511. }
  512. mediaType := rest[:semicolonIdx]
  513. rest = rest[semicolonIdx+1:]
  514. if !strings.HasPrefix(rest, "base64,") {
  515. return nil
  516. }
  517. data := strings.TrimPrefix(rest, "base64,")
  518. return &AnthropicImageSource{
  519. Type: "base64",
  520. MediaType: mediaType,
  521. Data: data,
  522. }
  523. }
  524. // mergeConsecutiveMessages merges consecutive messages with the same role
  525. // because Anthropic requires alternating user/assistant turns.
  526. func mergeConsecutiveMessages(messages []AnthropicMessage) []AnthropicMessage {
  527. if len(messages) <= 1 {
  528. return messages
  529. }
  530. var merged []AnthropicMessage
  531. for _, msg := range messages {
  532. if len(merged) == 0 || merged[len(merged)-1].Role != msg.Role {
  533. merged = append(merged, msg)
  534. continue
  535. }
  536. // Same role — merge content arrays
  537. last := &merged[len(merged)-1]
  538. lastBlocks := parseContentBlocks(last.Content)
  539. newBlocks := parseContentBlocks(msg.Content)
  540. combined := append(lastBlocks, newBlocks...)
  541. last.Content, _ = json.Marshal(combined)
  542. }
  543. return merged
  544. }
  545. // parseContentBlocks attempts to parse content as []AnthropicContentBlock.
  546. // If it's a string, wraps it in a text block.
  547. func parseContentBlocks(raw json.RawMessage) []AnthropicContentBlock {
  548. var blocks []AnthropicContentBlock
  549. if err := json.Unmarshal(raw, &blocks); err == nil {
  550. return blocks
  551. }
  552. var s string
  553. if err := json.Unmarshal(raw, &s); err == nil {
  554. return []AnthropicContentBlock{{Type: "text", Text: s}}
  555. }
  556. return nil
  557. }
  558. // convertResponsesToAnthropicTools maps Responses API tools to Anthropic format.
  559. // Reverse of convertAnthropicToolsToResponses.
  560. func convertResponsesToAnthropicTools(tools []ResponsesTool) []AnthropicTool {
  561. var out []AnthropicTool
  562. for _, t := range tools {
  563. switch t.Type {
  564. case "web_search", "google_search", "web_search_20250305":
  565. out = append(out, AnthropicTool{
  566. Type: "web_search_20250305",
  567. Name: "web_search",
  568. })
  569. case "function":
  570. out = append(out, AnthropicTool{
  571. Name: t.Name,
  572. Description: t.Description,
  573. InputSchema: normalizeAnthropicInputSchema(t.Parameters),
  574. })
  575. case "custom":
  576. out = append(out, AnthropicTool{
  577. Name: t.Name,
  578. Description: t.Description,
  579. InputSchema: normalizeAnthropicInputSchema(t.Parameters),
  580. })
  581. default:
  582. // Pass through unknown tool types
  583. out = append(out, AnthropicTool{
  584. Type: t.Type,
  585. Name: t.Name,
  586. Description: t.Description,
  587. InputSchema: normalizeAnthropicInputSchema(t.Parameters),
  588. })
  589. }
  590. }
  591. return out
  592. }
  593. // normalizeAnthropicInputSchema ensures input_schema is a valid object schema.
  594. func normalizeAnthropicInputSchema(schema json.RawMessage) json.RawMessage {
  595. const emptyObjectSchema = `{"type":"object","properties":{}}`
  596. trimmed := strings.TrimSpace(string(schema))
  597. if trimmed == "" || trimmed == "null" {
  598. return json.RawMessage(emptyObjectSchema)
  599. }
  600. var m map[string]json.RawMessage
  601. if err := json.Unmarshal(schema, &m); err != nil {
  602. return json.RawMessage(`{"type":"object","properties":{}}`)
  603. }
  604. typeRaw, ok := m["type"]
  605. if !ok || strings.TrimSpace(string(typeRaw)) == "" || string(typeRaw) == "null" {
  606. m["type"] = json.RawMessage(`"object"`)
  607. } else {
  608. var typ string
  609. if err := json.Unmarshal(typeRaw, &typ); err != nil || typ != "object" {
  610. return json.RawMessage(emptyObjectSchema)
  611. }
  612. }
  613. if _, ok := m["properties"]; !ok {
  614. m["properties"] = json.RawMessage(`{}`)
  615. }
  616. out, err := json.Marshal(m)
  617. if err != nil {
  618. return json.RawMessage(emptyObjectSchema)
  619. }
  620. return out
  621. }
  622. // convertResponsesToAnthropicToolChoice maps Responses tool_choice to Anthropic format.
  623. // Reverse of convertAnthropicToolChoiceToResponses.
  624. //
  625. // "auto" → {"type":"auto"}
  626. // "required" → {"type":"any"}
  627. // "none" → {"type":"none"}
  628. // {"type":"function","name":"X"} → {"type":"tool","name":"X"}
  629. // {"type":"function","function":{"name":"X"}} → {"type":"tool","name":"X"} // legacy
  630. func convertResponsesToAnthropicToolChoice(raw json.RawMessage) (json.RawMessage, error) {
  631. // Try as string first
  632. var s string
  633. if err := json.Unmarshal(raw, &s); err == nil {
  634. switch s {
  635. case "auto":
  636. return json.Marshal(map[string]string{"type": "auto"})
  637. case "required":
  638. return json.Marshal(map[string]string{"type": "any"})
  639. case "none":
  640. return json.Marshal(map[string]string{"type": "none"})
  641. default:
  642. return raw, nil
  643. }
  644. }
  645. // Try as object with type=function
  646. var tc struct {
  647. Type string `json:"type"`
  648. Name string `json:"name"`
  649. Function struct {
  650. Name string `json:"name"`
  651. } `json:"function"`
  652. }
  653. if err := json.Unmarshal(raw, &tc); err == nil && tc.Type == "function" {
  654. name := strings.TrimSpace(tc.Name)
  655. if name == "" {
  656. name = strings.TrimSpace(tc.Function.Name)
  657. }
  658. if name == "" {
  659. return raw, nil
  660. }
  661. return json.Marshal(map[string]string{
  662. "type": "tool",
  663. "name": name,
  664. })
  665. }
  666. // Pass through unknown
  667. return raw, nil
  668. }