chatcompletions_to_responses.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. )
  7. type chatMessageContent struct {
  8. Text *string
  9. Parts []ChatContentPart
  10. }
  11. // ChatCompletionsToResponses converts a Chat Completions request into a
  12. // Responses API request. The upstream always streams, so Stream is forced to
  13. // true. store is always false and reasoning.encrypted_content is always
  14. // included so that the response translator has full context.
  15. func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest, error) {
  16. input, err := convertChatMessagesToResponsesInput(req.Messages)
  17. if err != nil {
  18. return nil, err
  19. }
  20. inputJSON, err := json.Marshal(input)
  21. if err != nil {
  22. return nil, err
  23. }
  24. out := &ResponsesRequest{
  25. Model: req.Model,
  26. Instructions: req.Instructions,
  27. Input: inputJSON,
  28. Stream: true, // upstream always streams
  29. Include: []string{"reasoning.encrypted_content"},
  30. ServiceTier: req.ServiceTier,
  31. ParallelToolCalls: req.ParallelToolCalls,
  32. }
  33. // Reasoning models (gpt-5.x) do not accept sampling parameters.
  34. // See isReasoningModel in anthropic_to_responses.go.
  35. if !isReasoningModel(req.Model) {
  36. out.Temperature = req.Temperature
  37. out.TopP = req.TopP
  38. }
  39. storeFalse := false
  40. out.Store = &storeFalse
  41. // max_tokens / max_completion_tokens → max_output_tokens, prefer max_completion_tokens
  42. maxTokens := 0
  43. if req.MaxTokens != nil {
  44. maxTokens = *req.MaxTokens
  45. }
  46. if req.MaxCompletionTokens != nil {
  47. maxTokens = *req.MaxCompletionTokens
  48. }
  49. if maxTokens > 0 {
  50. v := maxTokens
  51. if v < minMaxOutputTokens {
  52. v = minMaxOutputTokens
  53. }
  54. out.MaxOutputTokens = &v
  55. }
  56. // reasoning_effort → reasoning.effort + reasoning.summary="auto"
  57. if req.ReasoningEffort != "" {
  58. out.Reasoning = &ResponsesReasoning{
  59. Effort: req.ReasoningEffort,
  60. Summary: "auto",
  61. }
  62. }
  63. if format := chatResponseFormatToResponsesTextFormat(req.ResponseFormat); len(format) > 0 {
  64. if out.Text == nil {
  65. out.Text = &ResponsesText{}
  66. }
  67. out.Text.Format = format
  68. }
  69. // tools[] and legacy functions[] → ResponsesTool[]
  70. if len(req.Tools) > 0 || len(req.Functions) > 0 {
  71. out.Tools = convertChatToolsToResponses(req.Tools, req.Functions)
  72. }
  73. // tool_choice: already compatible format — pass through directly.
  74. // Legacy function_call needs mapping.
  75. if len(req.ToolChoice) > 0 {
  76. out.ToolChoice = req.ToolChoice
  77. } else if len(req.FunctionCall) > 0 {
  78. tc, err := convertChatFunctionCallToToolChoice(req.FunctionCall)
  79. if err != nil {
  80. return nil, fmt.Errorf("convert function_call: %w", err)
  81. }
  82. out.ToolChoice = tc
  83. }
  84. return out, nil
  85. }
  86. // convertChatMessagesToResponsesInput converts the Chat Completions messages
  87. // array into a Responses API input items array.
  88. func convertChatMessagesToResponsesInput(msgs []ChatMessage) ([]ResponsesInputItem, error) {
  89. var out []ResponsesInputItem
  90. for _, m := range msgs {
  91. items, err := chatMessageToResponsesItems(m)
  92. if err != nil {
  93. return nil, err
  94. }
  95. out = append(out, items...)
  96. }
  97. return out, nil
  98. }
  99. // chatMessageToResponsesItems converts a single ChatMessage into one or more
  100. // ResponsesInputItem values.
  101. func chatMessageToResponsesItems(m ChatMessage) ([]ResponsesInputItem, error) {
  102. switch m.Role {
  103. case "system":
  104. return chatSystemToResponses(m)
  105. case "user":
  106. return chatUserToResponses(m)
  107. case "assistant":
  108. return chatAssistantToResponses(m)
  109. case "tool":
  110. return chatToolToResponses(m)
  111. case "function":
  112. return chatFunctionToResponses(m)
  113. default:
  114. return chatUserToResponses(m)
  115. }
  116. }
  117. // chatSystemToResponses converts a system message.
  118. func chatSystemToResponses(m ChatMessage) ([]ResponsesInputItem, error) {
  119. parsed, err := parseChatMessageContent(m.Content)
  120. if err != nil {
  121. return nil, err
  122. }
  123. content, err := marshalChatInputContent(parsed)
  124. if err != nil {
  125. return nil, err
  126. }
  127. return []ResponsesInputItem{{Role: "system", Content: content}}, nil
  128. }
  129. // chatUserToResponses converts a user message, handling both plain strings and
  130. // multi-modal content arrays.
  131. func chatUserToResponses(m ChatMessage) ([]ResponsesInputItem, error) {
  132. parsed, err := parseChatMessageContent(m.Content)
  133. if err != nil {
  134. return nil, fmt.Errorf("parse user content: %w", err)
  135. }
  136. content, err := marshalChatInputContent(parsed)
  137. if err != nil {
  138. return nil, err
  139. }
  140. return []ResponsesInputItem{{Role: "user", Content: content}}, nil
  141. }
  142. // chatAssistantToResponses converts an assistant message. If there is both
  143. // text content and tool_calls, the text is emitted as an assistant message
  144. // first, then each tool_call becomes a function_call item. If the content is
  145. // empty/nil and there are tool_calls, only function_call items are emitted.
  146. func chatAssistantToResponses(m ChatMessage) ([]ResponsesInputItem, error) {
  147. var items []ResponsesInputItem
  148. content := ""
  149. if m.ReasoningContent != "" {
  150. content = "<thinking>" + m.ReasoningContent + "</thinking>"
  151. }
  152. // Emit assistant message with output_text if content is non-empty.
  153. if len(m.Content) > 0 {
  154. s, err := parseAssistantContent(m.Content)
  155. if err != nil {
  156. return nil, err
  157. }
  158. if s != "" {
  159. if content != "" {
  160. content += "\n"
  161. }
  162. content += s
  163. }
  164. }
  165. if content != "" {
  166. parts := []ResponsesContentPart{{Type: "output_text", Text: content}}
  167. partsJSON, err := json.Marshal(parts)
  168. if err != nil {
  169. return nil, err
  170. }
  171. items = append(items, ResponsesInputItem{Role: "assistant", Content: partsJSON})
  172. }
  173. // Emit one function_call item per tool_call.
  174. for _, tc := range m.ToolCalls {
  175. args := tc.Function.Arguments
  176. if args == "" {
  177. args = "{}"
  178. }
  179. items = append(items, ResponsesInputItem{
  180. Type: "function_call",
  181. CallID: tc.ID,
  182. Name: tc.Function.Name,
  183. Arguments: args,
  184. })
  185. }
  186. return items, nil
  187. }
  188. // parseAssistantContent returns assistant content as plain text.
  189. //
  190. // Supported formats:
  191. // - JSON string
  192. // - JSON array of typed parts (e.g. [{"type":"text","text":"..."}])
  193. //
  194. // For structured thinking/reasoning parts, it preserves semantics by wrapping
  195. // the text in explicit tags so downstream can still distinguish it from normal text.
  196. func parseAssistantContent(raw json.RawMessage) (string, error) {
  197. if len(raw) == 0 {
  198. return "", nil
  199. }
  200. var s string
  201. if err := json.Unmarshal(raw, &s); err == nil {
  202. return s, nil
  203. }
  204. var parts []map[string]any
  205. if err := json.Unmarshal(raw, &parts); err != nil {
  206. // Keep compatibility with prior behavior: unsupported assistant content
  207. // formats are ignored instead of failing the whole request conversion.
  208. return "", nil
  209. }
  210. var b strings.Builder
  211. write := func(v string) error {
  212. _, err := b.WriteString(v)
  213. return err
  214. }
  215. for _, p := range parts {
  216. typ, _ := p["type"].(string)
  217. text, _ := p["text"].(string)
  218. thinking, _ := p["thinking"].(string)
  219. switch typ {
  220. case "thinking", "reasoning":
  221. if thinking != "" {
  222. if err := write("<thinking>"); err != nil {
  223. return "", err
  224. }
  225. if err := write(thinking); err != nil {
  226. return "", err
  227. }
  228. if err := write("</thinking>"); err != nil {
  229. return "", err
  230. }
  231. } else if text != "" {
  232. if err := write("<thinking>"); err != nil {
  233. return "", err
  234. }
  235. if err := write(text); err != nil {
  236. return "", err
  237. }
  238. if err := write("</thinking>"); err != nil {
  239. return "", err
  240. }
  241. }
  242. default:
  243. if text != "" {
  244. if err := write(text); err != nil {
  245. return "", err
  246. }
  247. }
  248. }
  249. }
  250. return b.String(), nil
  251. }
  252. // chatToolToResponses converts a tool result message (role=tool) into a
  253. // function_call_output item.
  254. func chatToolToResponses(m ChatMessage) ([]ResponsesInputItem, error) {
  255. output, err := parseChatContent(m.Content)
  256. if err != nil {
  257. return nil, err
  258. }
  259. if output == "" {
  260. output = "(empty)"
  261. }
  262. return []ResponsesInputItem{{
  263. Type: "function_call_output",
  264. CallID: m.ToolCallID,
  265. Output: output,
  266. }}, nil
  267. }
  268. // chatFunctionToResponses converts a legacy function result message
  269. // (role=function) into a function_call_output item. The Name field is used as
  270. // call_id since legacy function calls do not carry a separate call_id.
  271. func chatFunctionToResponses(m ChatMessage) ([]ResponsesInputItem, error) {
  272. output, err := parseChatContent(m.Content)
  273. if err != nil {
  274. return nil, err
  275. }
  276. if output == "" {
  277. output = "(empty)"
  278. }
  279. return []ResponsesInputItem{{
  280. Type: "function_call_output",
  281. CallID: m.Name,
  282. Output: output,
  283. }}, nil
  284. }
  285. // parseChatContent returns the string value of a ChatMessage Content field.
  286. // Content can be a JSON string or an array of typed parts. Array content is
  287. // flattened to text by concatenating text parts and ignoring non-text parts.
  288. func parseChatContent(raw json.RawMessage) (string, error) {
  289. parsed, err := parseChatMessageContent(raw)
  290. if err != nil {
  291. return "", err
  292. }
  293. if parsed.Text != nil {
  294. return *parsed.Text, nil
  295. }
  296. return flattenChatContentParts(parsed.Parts), nil
  297. }
  298. func parseChatMessageContent(raw json.RawMessage) (chatMessageContent, error) {
  299. if len(raw) == 0 {
  300. return chatMessageContent{Text: stringPtr("")}, nil
  301. }
  302. var s string
  303. if err := json.Unmarshal(raw, &s); err == nil {
  304. return chatMessageContent{Text: &s}, nil
  305. }
  306. var parts []ChatContentPart
  307. if err := json.Unmarshal(raw, &parts); err == nil {
  308. return chatMessageContent{Parts: parts}, nil
  309. }
  310. return chatMessageContent{}, fmt.Errorf("parse content as string or parts array")
  311. }
  312. func marshalChatInputContent(content chatMessageContent) (json.RawMessage, error) {
  313. if content.Text != nil {
  314. return json.Marshal(*content.Text)
  315. }
  316. parts := convertChatContentPartsToResponses(content.Parts)
  317. if len(parts) == 0 {
  318. // A nil slice marshals to JSON null, which the upstream Responses API
  319. // rejects ("expected an array of objects or string, but got null").
  320. // Fall back to an empty string when no usable parts remain.
  321. return json.Marshal("")
  322. }
  323. return json.Marshal(parts)
  324. }
  325. func convertChatContentPartsToResponses(parts []ChatContentPart) []ResponsesContentPart {
  326. var responseParts []ResponsesContentPart
  327. for _, p := range parts {
  328. switch p.Type {
  329. case "text":
  330. if p.Text != "" {
  331. responseParts = append(responseParts, ResponsesContentPart{
  332. Type: "input_text",
  333. Text: p.Text,
  334. })
  335. }
  336. case "image_url":
  337. if p.ImageURL != nil && p.ImageURL.URL != "" && !isEmptyBase64DataURI(p.ImageURL.URL) {
  338. responseParts = append(responseParts, ResponsesContentPart{
  339. Type: "input_image",
  340. ImageURL: p.ImageURL.URL,
  341. })
  342. }
  343. case "file":
  344. if p.File != nil && (p.File.FileData != "" || p.File.FileID != "") {
  345. responseParts = append(responseParts, ResponsesContentPart{
  346. Type: "input_file",
  347. Filename: p.File.Filename,
  348. FileData: p.File.FileData,
  349. FileID: p.File.FileID,
  350. })
  351. }
  352. }
  353. }
  354. return responseParts
  355. }
  356. func isEmptyBase64DataURI(raw string) bool {
  357. if !strings.HasPrefix(raw, "data:") {
  358. return false
  359. }
  360. rest := strings.TrimPrefix(raw, "data:")
  361. semicolonIdx := strings.Index(rest, ";")
  362. if semicolonIdx < 0 {
  363. return false
  364. }
  365. rest = rest[semicolonIdx+1:]
  366. if !strings.HasPrefix(rest, "base64,") {
  367. return false
  368. }
  369. return strings.TrimSpace(strings.TrimPrefix(rest, "base64,")) == ""
  370. }
  371. func flattenChatContentParts(parts []ChatContentPart) string {
  372. var textParts []string
  373. for _, p := range parts {
  374. if p.Type == "text" && p.Text != "" {
  375. textParts = append(textParts, p.Text)
  376. }
  377. }
  378. return strings.Join(textParts, "")
  379. }
  380. func stringPtr(s string) *string {
  381. return &s
  382. }
  383. // convertChatToolsToResponses maps Chat Completions tool definitions and legacy
  384. // function definitions to Responses API tool definitions.
  385. func convertChatToolsToResponses(tools []ChatTool, functions []ChatFunction) []ResponsesTool {
  386. var out []ResponsesTool
  387. for _, t := range tools {
  388. toolType := strings.ToLower(strings.TrimSpace(t.Type))
  389. if toolType == "x_search" {
  390. out = append(out, ResponsesTool{
  391. Type: "x_search",
  392. AllowedXHandles: t.AllowedXHandles,
  393. ExcludedXHandles: t.ExcludedXHandles,
  394. FromDate: t.FromDate,
  395. ToDate: t.ToDate,
  396. EnableImageUnderstanding: t.EnableImageUnderstanding,
  397. EnableVideoUnderstanding: t.EnableVideoUnderstanding,
  398. })
  399. continue
  400. }
  401. if toolType == "web_search" || toolType == "code_execution" {
  402. out = append(out, ResponsesTool{Type: toolType})
  403. continue
  404. }
  405. if t.Type != "function" || t.Function == nil {
  406. continue
  407. }
  408. rt := ResponsesTool{
  409. Type: "function",
  410. Name: t.Function.Name,
  411. Description: t.Function.Description,
  412. Parameters: t.Function.Parameters,
  413. Strict: defaultStrictFalse(t.Function.Strict),
  414. }
  415. out = append(out, rt)
  416. }
  417. // Legacy functions[] are treated as function-type tools.
  418. for _, f := range functions {
  419. rt := ResponsesTool{
  420. Type: "function",
  421. Name: f.Name,
  422. Description: f.Description,
  423. Parameters: f.Parameters,
  424. Strict: defaultStrictFalse(f.Strict),
  425. }
  426. out = append(out, rt)
  427. }
  428. return out
  429. }
  430. func defaultStrictFalse(src *bool) *bool {
  431. if src == nil {
  432. value := false
  433. return &value
  434. }
  435. return src
  436. }
  437. // convertChatFunctionCallToToolChoice maps the legacy function_call field to a
  438. // Responses API tool_choice value.
  439. //
  440. // "auto" → "auto"
  441. // "none" → "none"
  442. // {"name":"X"} → {"type":"function","name":"X"}
  443. func convertChatFunctionCallToToolChoice(raw json.RawMessage) (json.RawMessage, error) {
  444. // Try string first ("auto", "none", etc.) — pass through as-is.
  445. var s string
  446. if err := json.Unmarshal(raw, &s); err == nil {
  447. return json.Marshal(s)
  448. }
  449. // Object form: {"name":"X"}
  450. var obj struct {
  451. Name string `json:"name"`
  452. }
  453. if err := json.Unmarshal(raw, &obj); err != nil {
  454. return nil, err
  455. }
  456. return json.Marshal(map[string]any{
  457. "type": "function",
  458. "name": obj.Name,
  459. })
  460. }