chatcompletions_responses_bridge.go 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183
  1. package apicompat
  2. import (
  3. "bytes"
  4. "crypto/sha256"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "strings"
  9. "time"
  10. )
  11. const (
  12. toolOutputMediaMarker = "[Tool output media moved to the following user message]"
  13. toolOutputMediaAttribution = "[Tool output media for call %s]"
  14. )
  15. type toolOutputMediaByCallID map[string][]ChatContentPart
  16. // ResponsesToChatOptions carries optional hooks for
  17. // ResponsesToChatCompletionsRequestWithOptions. All fields are optional; a nil
  18. // *ResponsesToChatOptions behaves exactly like ResponsesToChatCompletionsRequest.
  19. type ResponsesToChatOptions struct {
  20. // ReasoningContentByID looks up the cached reasoning text for a reasoning
  21. // item id. Codex histories may carry reasoning items with no plaintext
  22. // summary (empty summary + opaque encrypted_content, e.g. after remote
  23. // compaction); DeepSeek's thinking mode rejects such histories with 400
  24. // "The `reasoning_content` in the thinking mode must be passed back to the
  25. // API". The gateway caches the reasoning text it streamed under the item
  26. // id, so the lookup restores the reasoning_content the client can no
  27. // longer provide. Return "" on a miss. A nil lookup keeps the original
  28. // behavior.
  29. ReasoningContentByID func(itemID string) string
  30. }
  31. // ResponsesToChatCompletionsRequest converts a Responses API request into a
  32. // Chat Completions request for upstreams that only implement
  33. // /v1/chat/completions.
  34. func ResponsesToChatCompletionsRequest(req *ResponsesRequest) (*ChatCompletionsRequest, error) {
  35. return ResponsesToChatCompletionsRequestWithOptions(req, nil)
  36. }
  37. // ResponsesToChatCompletionsRequestWithOptions is ResponsesToChatCompletionsRequest
  38. // with optional hooks (see ResponsesToChatOptions).
  39. func ResponsesToChatCompletionsRequestWithOptions(req *ResponsesRequest, opts *ResponsesToChatOptions) (*ChatCompletionsRequest, error) {
  40. if req == nil {
  41. return nil, fmt.Errorf("responses request is nil")
  42. }
  43. messages, err := responsesInputToChatMessagesWithOptions(req.Instructions, req.Input, opts)
  44. if err != nil {
  45. return nil, err
  46. }
  47. out := &ChatCompletionsRequest{
  48. Model: req.Model,
  49. Messages: messages,
  50. MaxCompletionTokens: req.MaxOutputTokens,
  51. Temperature: req.Temperature,
  52. TopP: req.TopP,
  53. Stream: req.Stream,
  54. ServiceTier: req.ServiceTier,
  55. ParallelToolCalls: req.ParallelToolCalls,
  56. }
  57. if req.Reasoning != nil {
  58. out.ReasoningEffort = req.Reasoning.Effort
  59. }
  60. effectiveTools, err := EffectiveResponsesTools(req)
  61. if err != nil {
  62. return nil, err
  63. }
  64. if len(effectiveTools) > 0 {
  65. tools, err := responsesToolsToChatTools(effectiveTools)
  66. if err != nil {
  67. return nil, err
  68. }
  69. out.Tools = tools
  70. }
  71. // tools 全部被丢弃(如仅含 web_search/image_generation 等服务端工具)时不再转发
  72. // tool_choice:上游会拒绝 "'tool_choice' is only allowed when 'tools' are specified"。
  73. // 指向被丢弃工具的选择项同理(见 responsesToolChoiceToChatToolChoice)。
  74. if len(out.Tools) > 0 && len(req.ToolChoice) > 0 {
  75. declared := make(map[string]bool, len(out.Tools))
  76. for _, tool := range out.Tools {
  77. if tool.Function != nil {
  78. declared[tool.Function.Name] = true
  79. }
  80. if strings.EqualFold(strings.TrimSpace(tool.Type), "x_search") {
  81. declared["x_search"] = true
  82. }
  83. }
  84. if tc := responsesToolChoiceToChatToolChoice(req.ToolChoice, declared); len(tc) > 0 {
  85. out.ToolChoice = tc
  86. }
  87. }
  88. if req.Text != nil {
  89. out.ResponseFormat = responsesTextFormatToChatResponseFormat(req.Text.Format)
  90. }
  91. return out, nil
  92. }
  93. // EffectiveResponsesTools returns every client-executable tool declared by a
  94. // Responses request. Newer Codex clients place their runtime tools in an
  95. // input item shaped as {"type":"additional_tools","tools":[...]} instead of
  96. // the top-level tools field. Chat-only upstreams must receive both forms.
  97. func EffectiveResponsesTools(req *ResponsesRequest) ([]ResponsesTool, error) {
  98. if req == nil {
  99. return nil, nil
  100. }
  101. tools := append([]ResponsesTool(nil), req.Tools...)
  102. inputRaw := bytesTrimSpace(req.Input)
  103. if len(inputRaw) == 0 || string(inputRaw) == "null" || inputRaw[0] != '[' {
  104. return tools, nil
  105. }
  106. var items []json.RawMessage
  107. if err := json.Unmarshal(inputRaw, &items); err != nil {
  108. return nil, fmt.Errorf("parse responses input for additional tools: %w", err)
  109. }
  110. for _, raw := range items {
  111. raw = bytesTrimSpace(raw)
  112. if len(raw) == 0 || raw[0] != '{' {
  113. continue
  114. }
  115. var discriminator struct {
  116. Type string `json:"type"`
  117. }
  118. if err := json.Unmarshal(raw, &discriminator); err != nil {
  119. return nil, fmt.Errorf("parse responses additional tools item: %w", err)
  120. }
  121. if discriminator.Type != "additional_tools" {
  122. continue
  123. }
  124. var item struct {
  125. Tools []ResponsesTool `json:"tools"`
  126. }
  127. if err := json.Unmarshal(raw, &item); err != nil {
  128. return nil, fmt.Errorf("parse responses additional tools item: %w", err)
  129. }
  130. tools = append(tools, item.Tools...)
  131. }
  132. // A completed client tool search can introduce tools for the remainder of
  133. // the turn. Promote them before lowering to ChatCompletions, using the same
  134. // dedupe, conflict, namespace, and custom-tool rules as the native adapter.
  135. toolsRaw, err := json.Marshal(tools)
  136. if err != nil {
  137. return nil, fmt.Errorf("encode responses tools for discovery promotion: %w", err)
  138. }
  139. var rawTools, rawInput []any
  140. if err := json.Unmarshal(toolsRaw, &rawTools); err != nil {
  141. return nil, fmt.Errorf("decode responses tools for discovery promotion: %w", err)
  142. }
  143. if err := json.Unmarshal(inputRaw, &rawInput); err != nil {
  144. return nil, fmt.Errorf("parse responses input for discovery promotion: %w", err)
  145. }
  146. promoted, err := promotedResponsesToolSearchDiscoveries(rawTools, rawInput)
  147. if err != nil {
  148. return nil, err
  149. }
  150. if len(promoted) > 0 {
  151. promotedRaw, err := json.Marshal(promoted)
  152. if err != nil {
  153. return nil, fmt.Errorf("encode promoted responses tools: %w", err)
  154. }
  155. var discovered []ResponsesTool
  156. if err := json.Unmarshal(promotedRaw, &discovered); err != nil {
  157. return nil, fmt.Errorf("decode promoted responses tools: %w", err)
  158. }
  159. tools = append(tools, discovered...)
  160. }
  161. return tools, nil
  162. }
  163. // CustomToolNames 收集 Responses 请求中 custom/freeform 工具的名字。chat 桥回程时
  164. // 需要据此把模型对这些工具的调用还原为 custom_tool_call 项(codex 只按该类型路由)。
  165. func CustomToolNames(tools []ResponsesTool) map[string]bool {
  166. var out map[string]bool
  167. for _, tool := range tools {
  168. if tool.Type == "custom" && tool.Name != "" {
  169. if out == nil {
  170. out = make(map[string]bool)
  171. }
  172. out[tool.Name] = true
  173. }
  174. }
  175. return out
  176. }
  177. // FunctionToolNames collects explicitly declared top-level function tools.
  178. func FunctionToolNames(tools []ResponsesTool) map[string]bool {
  179. var out map[string]bool
  180. for _, tool := range tools {
  181. if tool.Type == "function" && tool.Name != "" {
  182. if out == nil {
  183. out = make(map[string]bool)
  184. }
  185. out[tool.Name] = true
  186. }
  187. }
  188. return out
  189. }
  190. // NamespacedToolName 记录 namespace 子工具的原始归属(命名空间 + 裸子工具名)。
  191. type NamespacedToolName struct {
  192. Namespace string
  193. Name string
  194. }
  195. // NamespaceToolNames 收集 Responses 请求中 namespace 子工具的摊平名 →(namespace,
  196. // 子工具名)映射。chat 桥回程时需据此把模型对摊平工具的调用还原为带 namespace 字段
  197. // 的 function_call 项:codex 按 namespace+name 路由,平铺名会被判为 unsupported
  198. // call;摊平名超长时带截断哈希(见 flattenNamespaceToolName),无法按字符串切分还原。
  199. // 摊平名撞名的请求已在转换阶段被显式拒绝(见 namespaceChildrenToChatTools),
  200. // 此处映射不存在歧义。
  201. func NamespaceToolNames(tools []ResponsesTool) map[string]NamespacedToolName {
  202. var out map[string]NamespacedToolName
  203. for _, tool := range tools {
  204. if tool.Type != "namespace" || tool.Name == "" {
  205. continue
  206. }
  207. children := tool.Tools
  208. if len(children) == 0 {
  209. children = tool.Children
  210. }
  211. for _, child := range children {
  212. if child.Type != "function" || child.Name == "" {
  213. continue
  214. }
  215. if out == nil {
  216. out = make(map[string]NamespacedToolName)
  217. }
  218. out[flattenNamespaceToolName(tool.Name, child.Name)] = NamespacedToolName{
  219. Namespace: tool.Name,
  220. Name: child.Name,
  221. }
  222. }
  223. }
  224. return out
  225. }
  226. // customToolCallName restores both the exact downgraded custom-tool name and
  227. // namespace-prefixed aliases that chat models sometimes infer from neighboring
  228. // flattened namespace tools (for example functions__exec beside
  229. // functions__wait). A real declared namespace child always owns its flattened
  230. // name, and ambiguous aliases are left as ordinary function calls.
  231. func customToolCallName(name string, customTools, functionTools map[string]bool, namespaceTools map[string]NamespacedToolName) (string, bool) {
  232. if functionTools[name] {
  233. return "", false
  234. }
  235. if customTools[name] {
  236. return name, true
  237. }
  238. if _, ok := namespaceTools[name]; ok {
  239. return "", false
  240. }
  241. match := ""
  242. for customName := range customTools {
  243. for _, namespaceTool := range namespaceTools {
  244. if flattenNamespaceToolName(namespaceTool.Namespace, customName) != name {
  245. continue
  246. }
  247. if match != "" && match != customName {
  248. return "", false
  249. }
  250. match = customName
  251. }
  252. }
  253. return match, match != ""
  254. }
  255. func customNameForStreamTool(state *ChatCompletionsToResponsesStreamState, name string) string {
  256. if customName, ok := customToolCallName(name, state.CustomTools, state.FunctionTools, state.NamespaceTools); ok {
  257. return customName
  258. }
  259. return name
  260. }
  261. // HasToolSearchTool 判断 Responses 请求是否声明了 tool_search 服务端工具。chat 桥
  262. // 回程时需据此把模型对代理工具的调用还原为 tool_search_call 项:codex 只在该项类型
  263. // 且 execution=client 时执行 tool search,同名 function_call 会因 payload 不匹配
  264. // 触发 fatal 中止整个 turn。
  265. func HasToolSearchTool(tools []ResponsesTool) bool {
  266. for _, tool := range tools {
  267. if tool.Type == "tool_search" {
  268. return true
  269. }
  270. }
  271. return false
  272. }
  273. // responsesInputToChatMessages converts a Responses request's instructions +
  274. // input[] into Chat Completions messages. It is a three-stage pipeline:
  275. //
  276. // parse — instructions become a system message; input[] is split into items
  277. // build — buildChatMessagesFromItems walks items, attaching reasoning to the
  278. // assistant message that produced a tool call, merging parallel tool
  279. // calls into one assistant message, and skipping item types that have
  280. // no Chat equivalent
  281. // normalize — normalizeChatMessages enforces the invariants DeepSeek requires
  282. //
  283. // The build + normalize split keeps every protocol rule in one place rather than
  284. // scattered across per-item cases, and makes unknown future codex item types
  285. // fail safe instead of leaking into the upstream request.
  286. func responsesInputToChatMessages(instructions string, inputRaw json.RawMessage) ([]ChatMessage, error) {
  287. return responsesInputToChatMessagesWithOptions(instructions, inputRaw, nil)
  288. }
  289. // responsesInputToChatMessagesWithOptions is responsesInputToChatMessages with
  290. // optional hooks (see ResponsesToChatOptions).
  291. func responsesInputToChatMessagesWithOptions(instructions string, inputRaw json.RawMessage, opts *ResponsesToChatOptions) ([]ChatMessage, error) {
  292. var messages []ChatMessage
  293. if strings.TrimSpace(instructions) != "" {
  294. content, _ := json.Marshal(instructions)
  295. messages = append(messages, ChatMessage{Role: "system", Content: content})
  296. }
  297. inputRaw = bytesTrimSpace(inputRaw)
  298. if len(inputRaw) == 0 || string(inputRaw) == "null" {
  299. return messages, nil
  300. }
  301. // Bare string input is a single user turn.
  302. var inputText string
  303. if err := json.Unmarshal(inputRaw, &inputText); err == nil {
  304. content, _ := json.Marshal(inputText)
  305. messages = append(messages, ChatMessage{Role: "user", Content: content})
  306. return messages, nil
  307. }
  308. var rawItems []json.RawMessage
  309. if err := json.Unmarshal(inputRaw, &rawItems); err != nil {
  310. return nil, fmt.Errorf("parse responses input: %w", err)
  311. }
  312. built, mediaByCallID, err := buildChatMessagesFromItems(messages, rawItems, opts)
  313. if err != nil {
  314. return nil, err
  315. }
  316. return normalizeChatMessagesWithToolOutputMedia(built, mediaByCallID), nil
  317. }
  318. // buildChatMessagesFromItems walks the Responses input items and appends the
  319. // corresponding Chat messages.
  320. func buildChatMessagesFromItems(messages []ChatMessage, rawItems []json.RawMessage, opts *ResponsesToChatOptions) ([]ChatMessage, toolOutputMediaByCallID, error) {
  321. // pendingReasoning holds the reasoning text from a reasoning item until the
  322. // assistant message it belongs to is emitted. DeepSeek's thinking mode
  323. // requires the reasoning_content that produced a tool call to be passed back
  324. // on that assistant message; dropping it yields a 400. It only survives
  325. // across an assistant message (so a following tool call in the same turn
  326. // still receives it); any other role ends the thinking span.
  327. var pendingReasoning string
  328. // lastTurnReasoning is the most recent reasoning text of the current turn,
  329. // surviving tool outputs. DeepSeek emits reasoning only once per turn, so
  330. // chained tool calls (reasoning → call A → output A → call B) leave call B's
  331. // assistant message without reasoning_content and DeepSeek 400s the history;
  332. // replaying the turn's reasoning on B's message satisfies the contract. Only
  333. // a user-side item ends the turn and clears it.
  334. var lastTurnReasoning string
  335. mediaByCallID := make(toolOutputMediaByCallID)
  336. invalidFunctionCallIDs := make(map[string]struct{})
  337. invalidEmptyFunctionCallOutputs := 0
  338. reasoningForAssistant := func() string {
  339. if pendingReasoning != "" {
  340. return pendingReasoning
  341. }
  342. return lastTurnReasoning
  343. }
  344. for _, raw := range rawItems {
  345. raw = bytesTrimSpace(raw)
  346. if len(raw) == 0 || string(raw) == "null" {
  347. continue
  348. }
  349. var item map[string]json.RawMessage
  350. if err := json.Unmarshal(raw, &item); err != nil {
  351. var text string
  352. if textErr := json.Unmarshal(raw, &text); textErr == nil {
  353. content, _ := json.Marshal(text)
  354. messages = append(messages, ChatMessage{Role: "user", Content: content})
  355. pendingReasoning = ""
  356. lastTurnReasoning = ""
  357. continue
  358. }
  359. return nil, nil, fmt.Errorf("parse responses input item: %w", err)
  360. }
  361. role := chatCompletionsBridgeRole(rawString(item["role"]))
  362. itemType := rawString(item["type"])
  363. switch itemType {
  364. case "reasoning":
  365. if txt := extractResponsesReasoningText(item); txt != "" {
  366. pendingReasoning = txt
  367. } else if opts != nil && opts.ReasoningContentByID != nil {
  368. // No plaintext summary (encrypted-only reasoning, e.g. after codex
  369. // remote compaction): fall back to the gateway-side cache keyed
  370. // by the reasoning item id, which always round-trips in history.
  371. if id := rawString(item["id"]); id != "" {
  372. if cached := opts.ReasoningContentByID(id); cached != "" {
  373. pendingReasoning = cached
  374. }
  375. }
  376. }
  377. if pendingReasoning != "" {
  378. lastTurnReasoning = pendingReasoning
  379. }
  380. continue
  381. case "function_call":
  382. arguments := rawString(item["arguments"])
  383. if strings.TrimSpace(arguments) == "" {
  384. arguments = "{}"
  385. }
  386. callID := rawString(item["call_id"])
  387. if !json.Valid([]byte(arguments)) {
  388. // A previous streamed turn can leave a truncated function_call in
  389. // Codex history (for example after an upstream SSE parse failure or
  390. // an output-limit interruption). Do not forward that item to a
  391. // Chat Completions provider, which rejects the entire request. Its
  392. // matching output is skipped below as well, allowing the next user
  393. // turn to self-heal instead of repeatedly replaying the poison.
  394. if callID != "" {
  395. invalidFunctionCallIDs[callID] = struct{}{}
  396. } else {
  397. invalidEmptyFunctionCallOutputs++
  398. }
  399. pendingReasoning = ""
  400. continue
  401. }
  402. name := rawString(item["name"])
  403. // namespace 子工具的历史调用带 namespace 字段,需与请求方向的摊平
  404. // 命名(namespaceChildrenToChatTools)保持一致。
  405. if ns := rawString(item["namespace"]); ns != "" {
  406. name = flattenNamespaceToolName(ns, name)
  407. }
  408. toolCall := ChatToolCall{
  409. ID: callID,
  410. Type: "function",
  411. Function: ChatFunctionCall{
  412. Name: name,
  413. Arguments: arguments,
  414. },
  415. }
  416. messages = appendAssistantToolCall(messages, toolCall, reasoningForAssistant())
  417. pendingReasoning = ""
  418. continue
  419. case "tool_search_call":
  420. // tool_search 调用的 arguments 是 JSON 对象(如 {"query": ...}),
  421. // 原文即为降级 function 调用的 arguments 字符串。
  422. arguments := strings.TrimSpace(string(bytesTrimSpace(item["arguments"])))
  423. if s := rawString(item["arguments"]); s != "" {
  424. arguments = s
  425. }
  426. if arguments == "" || arguments == "null" {
  427. arguments = "{}"
  428. }
  429. toolCall := ChatToolCall{
  430. ID: rawString(item["call_id"]),
  431. Type: "function",
  432. Function: ChatFunctionCall{
  433. Name: toolSearchProxyName,
  434. Arguments: arguments,
  435. },
  436. }
  437. messages = appendAssistantToolCall(messages, toolCall, reasoningForAssistant())
  438. pendingReasoning = ""
  439. continue
  440. case "custom_tool_call":
  441. // custom/freeform 工具的历史调用:input 自由文本包进降级 function 工具
  442. // 的 {"input": ...} 参数,与请求方向的工具降级(customToolInputSchema)
  443. // 保持一致,模型才能把历史与当前工具定义对上。
  444. arguments, _ := json.Marshal(map[string]string{"input": rawString(item["input"])})
  445. toolCall := ChatToolCall{
  446. ID: rawString(item["call_id"]),
  447. Type: "function",
  448. Function: ChatFunctionCall{
  449. Name: rawString(item["name"]),
  450. Arguments: string(arguments),
  451. },
  452. }
  453. messages = appendAssistantToolCall(messages, toolCall, reasoningForAssistant())
  454. pendingReasoning = ""
  455. continue
  456. case "function_call_output", "custom_tool_call_output", "tool_search_output":
  457. outputRaw := bytesTrimSpace(item["output"])
  458. if itemType == "tool_search_output" && (len(outputRaw) == 0 || string(outputRaw) == "null") {
  459. // Newer clients return discoveries in tools[] without a separate
  460. // output field. Keep that useful result in Chat tool history.
  461. outputRaw = bytesTrimSpace(item["tools"])
  462. }
  463. callID := rawString(item["call_id"])
  464. if callID == "" && invalidEmptyFunctionCallOutputs > 0 {
  465. invalidEmptyFunctionCallOutputs--
  466. pendingReasoning = ""
  467. continue
  468. }
  469. if _, skipped := invalidFunctionCallIDs[callID]; skipped {
  470. pendingReasoning = ""
  471. continue
  472. }
  473. delete(mediaByCallID, callID)
  474. outputText, media, rewritten := extractToolOutputMedia(outputRaw)
  475. if rewritten {
  476. if callID != "" {
  477. mediaByCallID[callID] = media
  478. }
  479. } else {
  480. outputText = rawString(outputRaw)
  481. if outputText == "" && len(outputRaw) > 0 && string(outputRaw) != "null" && string(outputRaw) != `""` {
  482. // 对象/数组形式的输出(如 tool_search 的结果列表)整体字符串化。
  483. outputText = string(outputRaw)
  484. }
  485. }
  486. content, _ := json.Marshal(outputText)
  487. messages = append(messages, ChatMessage{
  488. Role: "tool",
  489. ToolCallID: callID,
  490. Content: content,
  491. })
  492. pendingReasoning = ""
  493. continue
  494. case "input_text", "text":
  495. content, _ := json.Marshal(rawString(item["text"]))
  496. messages = append(messages, ChatMessage{Role: "user", Content: content})
  497. pendingReasoning = ""
  498. lastTurnReasoning = ""
  499. continue
  500. case "input_image":
  501. content, err := chatContentFromSingleResponsesPart(itemType, item)
  502. if err != nil {
  503. return nil, nil, err
  504. }
  505. messages = append(messages, ChatMessage{Role: "user", Content: content})
  506. pendingReasoning = ""
  507. lastTurnReasoning = ""
  508. continue
  509. }
  510. // Only genuine message items become chat messages. Codex emits other
  511. // Responses item types with no Chat equivalent (web_search_call,
  512. // local_shell_call, file_search_call, ...). Converting them via the
  513. // generic path would insert a spurious message between an assistant
  514. // tool_calls message and its tool reply, which DeepSeek rejects
  515. // ("insufficient tool messages following tool_calls message"). Skip them.
  516. if itemType != "" && itemType != "message" {
  517. pendingReasoning = ""
  518. continue
  519. }
  520. content := item["content"]
  521. if len(bytesTrimSpace(content)) == 0 {
  522. if text := rawString(item["text"]); text != "" {
  523. content, _ = json.Marshal(text)
  524. }
  525. }
  526. chatContent, err := responsesContentToChatContent(content, role)
  527. if err != nil {
  528. return nil, nil, err
  529. }
  530. msg := ChatMessage{Role: role, Content: chatContent}
  531. // DeepSeek thinking mode requires the reasoning_content from a prior
  532. // reasoning-only / plain-text assistant turn to be passed back on its
  533. // assistant message; dropping it yields 400 "The `reasoning_content` in
  534. // the thinking mode must be passed back to the API" on the next turn.
  535. // A following function_call in the same turn still receives it because
  536. // appendAssistantToolCall merges into this message and only fills
  537. // ReasoningContent when it is still empty.
  538. if role == "assistant" {
  539. msg.ReasoningContent = reasoningForAssistant()
  540. pendingReasoning = ""
  541. } else {
  542. pendingReasoning = ""
  543. lastTurnReasoning = ""
  544. }
  545. messages = append(messages, msg)
  546. }
  547. return messages, mediaByCallID, nil
  548. }
  549. // extractToolOutputMedia rewrites only recognized image nodes. Media-free
  550. // outputs return rewritten=false so the caller can preserve their original
  551. // bytes and prompt-cache prefix.
  552. func extractToolOutputMedia(outputRaw json.RawMessage) (string, []ChatContentPart, bool) {
  553. outputRaw = bytesTrimSpace(outputRaw)
  554. if len(outputRaw) == 0 || string(outputRaw) == "null" {
  555. return "", nil, false
  556. }
  557. var outputString string
  558. if err := json.Unmarshal(outputRaw, &outputString); err == nil {
  559. if isToolOutputImageDataURL(outputString) {
  560. return toolOutputMediaMarker, []ChatContentPart{toolOutputImagePart(outputString)}, true
  561. }
  562. nested, ok := decodeToolOutputJSON([]byte(outputString))
  563. if !ok {
  564. return "", nil, false
  565. }
  566. rewritten, media, changed := rewriteToolOutputMediaValue(nested)
  567. if !changed {
  568. return "", nil, false
  569. }
  570. encoded, err := json.Marshal(rewritten)
  571. if err != nil {
  572. return "", nil, false
  573. }
  574. return string(encoded), media, true
  575. }
  576. value, ok := decodeToolOutputJSON(outputRaw)
  577. if !ok {
  578. return "", nil, false
  579. }
  580. rewritten, media, changed := rewriteToolOutputMediaValue(value)
  581. if !changed {
  582. return "", nil, false
  583. }
  584. encoded, err := json.Marshal(rewritten)
  585. if err != nil {
  586. return "", nil, false
  587. }
  588. return string(encoded), media, true
  589. }
  590. func decodeToolOutputJSON(raw []byte) (any, bool) {
  591. if !json.Valid(raw) {
  592. return nil, false
  593. }
  594. decoder := json.NewDecoder(bytes.NewReader(raw))
  595. decoder.UseNumber()
  596. var value any
  597. if err := decoder.Decode(&value); err != nil {
  598. return nil, false
  599. }
  600. return value, true
  601. }
  602. func rewriteToolOutputMediaValue(value any) (any, []ChatContentPart, bool) {
  603. switch typed := value.(type) {
  604. case []any:
  605. var media []ChatContentPart
  606. changed := false
  607. for i, item := range typed {
  608. rewritten, itemMedia, itemChanged := rewriteToolOutputMediaValue(item)
  609. if !itemChanged {
  610. continue
  611. }
  612. typed[i] = rewritten
  613. media = append(media, itemMedia...)
  614. changed = true
  615. }
  616. return typed, media, changed
  617. case map[string]any:
  618. if imageURL, ok := recognizedToolOutputImageURL(typed); ok {
  619. return map[string]any{
  620. "type": "input_text",
  621. "text": toolOutputMediaMarker,
  622. }, []ChatContentPart{toolOutputImagePart(imageURL)}, true
  623. }
  624. content, ok := typed["content"]
  625. if !ok {
  626. return typed, nil, false
  627. }
  628. rewritten, media, changed := rewriteToolOutputMediaValue(content)
  629. if !changed {
  630. return typed, nil, false
  631. }
  632. typed["content"] = rewritten
  633. return typed, media, true
  634. default:
  635. return value, nil, false
  636. }
  637. }
  638. func recognizedToolOutputImageURL(value map[string]any) (string, bool) {
  639. partType, _ := value["type"].(string)
  640. if partType != "input_image" && partType != "image_url" {
  641. return "", false
  642. }
  643. switch imageURL := value["image_url"].(type) {
  644. case string:
  645. return imageURL, strings.TrimSpace(imageURL) != ""
  646. case map[string]any:
  647. url, _ := imageURL["url"].(string)
  648. return url, strings.TrimSpace(url) != ""
  649. default:
  650. return "", false
  651. }
  652. }
  653. func isToolOutputImageDataURL(value string) bool {
  654. const prefix = "data:image/"
  655. const separator = ";base64,"
  656. if !strings.HasPrefix(value, prefix) {
  657. return false
  658. }
  659. separatorIndex := strings.Index(value[len(prefix):], separator)
  660. if separatorIndex <= 0 {
  661. return false
  662. }
  663. payloadIndex := len(prefix) + separatorIndex + len(separator)
  664. return payloadIndex < len(value)
  665. }
  666. func toolOutputImagePart(imageURL string) ChatContentPart {
  667. return ChatContentPart{
  668. Type: "image_url",
  669. ImageURL: &ChatImageURL{URL: imageURL},
  670. }
  671. }
  672. // appendAssistantToolCall merges a tool call into the chat message list.
  673. // Parallel tool calls arrive as consecutive *_call items and must share one
  674. // assistant message; the matching tool replies then follow it. Merge into the
  675. // immediately preceding assistant message.
  676. func appendAssistantToolCall(messages []ChatMessage, toolCall ChatToolCall, pendingReasoning string) []ChatMessage {
  677. if n := len(messages); n > 0 && messages[n-1].Role == "assistant" {
  678. messages[n-1].ToolCalls = append(messages[n-1].ToolCalls, toolCall)
  679. if messages[n-1].ReasoningContent == "" {
  680. messages[n-1].ReasoningContent = pendingReasoning
  681. }
  682. return messages
  683. }
  684. return append(messages, ChatMessage{
  685. Role: "assistant",
  686. ToolCalls: []ChatToolCall{toolCall},
  687. ReasoningContent: pendingReasoning,
  688. })
  689. }
  690. // normalizeChatMessages is the single place that enforces the tool-call
  691. // invariant the DeepSeek / OpenAI Chat Completions schema requires: an assistant
  692. // message with tool_calls must be immediately followed by one tool message per
  693. // tool_call_id, in order, with nothing in between.
  694. //
  695. // Codex histories violate this in several ways that the builder alone can't fix:
  696. // - a non-tool message lands between an assistant tool_calls message and its
  697. // tool replies (e.g. an "Approved command prefix saved" system notice codex
  698. // injects mid tool-execution);
  699. // - a parallel tool_call's sibling output never arrives, or a call is left
  700. // dangling by a mid-execution reconnect (unanswered tool_call);
  701. // - a tool reply has no announcing assistant tool_call (orphan).
  702. //
  703. // It rebuilds the sequence so each assistant's answered tool_calls are followed
  704. // directly by their replies (in call order); unanswered tool_calls are dropped
  705. // (and an assistant left with neither tool_calls nor content is dropped); orphan
  706. // tool replies and intervening messages are emitted in their natural position
  707. // but never between an assistant tool_calls message and its replies.
  708. func normalizeChatMessages(messages []ChatMessage) []ChatMessage {
  709. return normalizeChatMessagesWithToolOutputMedia(messages, nil)
  710. }
  711. func normalizeChatMessagesWithToolOutputMedia(messages []ChatMessage, mediaByCallID toolOutputMediaByCallID) []ChatMessage {
  712. // Index every tool reply by its tool_call_id (last wins on duplicates).
  713. replies := make(map[string]ChatMessage)
  714. for _, m := range messages {
  715. if m.Role == "tool" && m.ToolCallID != "" {
  716. replies[m.ToolCallID] = m
  717. }
  718. }
  719. out := make([]ChatMessage, 0, len(messages))
  720. for _, m := range messages {
  721. switch {
  722. case m.Role == "tool":
  723. // A bare tool message with no tool_call_id is a direct Chat
  724. // Completions passthrough; keep it in place. A tool reply whose id is
  725. // announced by an assistant is emitted right after that assistant
  726. // (skip the standalone occurrence). Any other tool reply is an orphan
  727. // and is dropped.
  728. if m.ToolCallID == "" {
  729. out = append(out, m)
  730. }
  731. continue
  732. case len(m.ToolCalls) > 0:
  733. kept := make([]ChatToolCall, 0, len(m.ToolCalls))
  734. for _, tc := range m.ToolCalls {
  735. if tc.ID == "" {
  736. continue
  737. }
  738. if _, ok := replies[tc.ID]; ok {
  739. kept = append(kept, tc)
  740. }
  741. }
  742. if len(kept) == 0 {
  743. // No answered tool_calls left: keep as a plain message if it has
  744. // content, otherwise drop it entirely.
  745. if isBlankChatContent(m.Content) {
  746. continue
  747. }
  748. m.ToolCalls = nil
  749. out = append(out, m)
  750. continue
  751. }
  752. m.ToolCalls = kept
  753. out = append(out, m)
  754. for _, tc := range kept {
  755. out = append(out, replies[tc.ID])
  756. }
  757. var mediaParts []ChatContentPart
  758. for _, tc := range kept {
  759. media := mediaByCallID[tc.ID]
  760. if len(media) == 0 {
  761. continue
  762. }
  763. mediaParts = append(mediaParts, ChatContentPart{
  764. Type: "text",
  765. Text: fmt.Sprintf(toolOutputMediaAttribution, tc.ID),
  766. })
  767. mediaParts = append(mediaParts, media...)
  768. }
  769. if len(mediaParts) > 0 {
  770. content, _ := json.Marshal(mediaParts)
  771. out = append(out, ChatMessage{Role: "user", Content: content})
  772. }
  773. default:
  774. out = append(out, m)
  775. }
  776. }
  777. return out
  778. }
  779. // isBlankChatContent reports whether a chat message content holds no usable text.
  780. func isBlankChatContent(raw json.RawMessage) bool {
  781. raw = bytesTrimSpace(raw)
  782. if len(raw) == 0 || string(raw) == "null" || string(raw) == `""` {
  783. return true
  784. }
  785. return chatMessageContentText(raw) == ""
  786. }
  787. // extractResponsesReasoningText pulls the reasoning text out of a Responses
  788. // reasoning item. The Chat→Responses bridge writes the upstream reasoning_content
  789. // verbatim into the summary_text parts (see closeChatReasoningItem), so codex
  790. // round-trips it there; prefer summary[].text and fall back to content.
  791. func extractResponsesReasoningText(item map[string]json.RawMessage) string {
  792. var parts []string
  793. collect := func(raw json.RawMessage) {
  794. raw = bytesTrimSpace(raw)
  795. if len(raw) == 0 || string(raw) == "null" {
  796. return
  797. }
  798. var arr []map[string]json.RawMessage
  799. if err := json.Unmarshal(raw, &arr); err == nil {
  800. for _, p := range arr {
  801. if t := rawString(p["text"]); t != "" {
  802. parts = append(parts, t)
  803. }
  804. }
  805. return
  806. }
  807. if t := rawString(raw); t != "" {
  808. parts = append(parts, t)
  809. }
  810. }
  811. collect(item["summary"])
  812. if len(parts) == 0 {
  813. collect(item["content"])
  814. }
  815. return strings.Join(parts, "\n")
  816. }
  817. // ExtractResponsesReasoningItem parses a raw Responses input item and, when it
  818. // is a reasoning item, returns its id and extractable plaintext (summary
  819. // preferred, content fallback). ok is false for non-reasoning items. It exists
  820. // for the gateway-side reasoning cache: items with plaintext get (re)cached so
  821. // later encrypted-only replicas of the same item id can be restored.
  822. func ExtractResponsesReasoningItem(raw json.RawMessage) (id string, text string, ok bool) {
  823. raw = bytesTrimSpace(raw)
  824. if len(raw) == 0 || string(raw) == "null" {
  825. return "", "", false
  826. }
  827. var item map[string]json.RawMessage
  828. if err := json.Unmarshal(raw, &item); err != nil {
  829. return "", "", false
  830. }
  831. if rawString(item["type"]) != "reasoning" {
  832. return "", "", false
  833. }
  834. return rawString(item["id"]), extractResponsesReasoningText(item), true
  835. }
  836. func chatCompletionsBridgeRole(role string) string {
  837. trimmed := strings.TrimSpace(role)
  838. if trimmed == "" {
  839. return "user"
  840. }
  841. if strings.EqualFold(trimmed, "developer") {
  842. return "system"
  843. }
  844. return role
  845. }
  846. func responsesContentToChatContent(raw json.RawMessage, role string) (json.RawMessage, error) {
  847. raw = bytesTrimSpace(raw)
  848. if len(raw) == 0 || string(raw) == "null" {
  849. empty, _ := json.Marshal("")
  850. return empty, nil
  851. }
  852. var text string
  853. if err := json.Unmarshal(raw, &text); err == nil {
  854. return raw, nil
  855. }
  856. var rawParts []json.RawMessage
  857. if err := json.Unmarshal(raw, &rawParts); err == nil {
  858. return responsesContentPartsToChatContent(rawParts, role)
  859. }
  860. var obj map[string]json.RawMessage
  861. if err := json.Unmarshal(raw, &obj); err == nil {
  862. return chatContentFromSingleResponsesPart(rawString(obj["type"]), obj)
  863. }
  864. return raw, nil
  865. }
  866. func responsesContentPartsToChatContent(rawParts []json.RawMessage, role string) (json.RawMessage, error) {
  867. var textParts []string
  868. var chatParts []ChatContentPart
  869. hasNonText := false
  870. for _, rawPart := range rawParts {
  871. var part map[string]json.RawMessage
  872. if err := json.Unmarshal(rawPart, &part); err != nil {
  873. continue
  874. }
  875. partType := rawString(part["type"])
  876. switch partType {
  877. case "input_text", "output_text", "text", "":
  878. text := rawString(part["text"])
  879. if text == "" {
  880. continue
  881. }
  882. textParts = append(textParts, text)
  883. chatParts = append(chatParts, ChatContentPart{Type: "text", Text: text})
  884. case "input_image", "image_url":
  885. imageURL := rawString(part["image_url"])
  886. if imageURL == "" {
  887. imageURL = rawNestedString(part["image_url"], "url")
  888. }
  889. if imageURL == "" {
  890. continue
  891. }
  892. hasNonText = true
  893. chatParts = append(chatParts, ChatContentPart{
  894. Type: "image_url",
  895. ImageURL: &ChatImageURL{URL: imageURL},
  896. })
  897. }
  898. }
  899. if !hasNonText {
  900. joined, _ := json.Marshal(strings.Join(textParts, "\n\n"))
  901. return joined, nil
  902. }
  903. if role != "user" {
  904. joined, _ := json.Marshal(strings.Join(textParts, "\n\n"))
  905. return joined, nil
  906. }
  907. if len(chatParts) == 0 {
  908. empty, _ := json.Marshal("")
  909. return empty, nil
  910. }
  911. return json.Marshal(chatParts)
  912. }
  913. func chatContentFromSingleResponsesPart(partType string, part map[string]json.RawMessage) (json.RawMessage, error) {
  914. switch partType {
  915. case "input_image", "image_url":
  916. imageURL := rawString(part["image_url"])
  917. if imageURL == "" {
  918. imageURL = rawNestedString(part["image_url"], "url")
  919. }
  920. return json.Marshal([]ChatContentPart{{
  921. Type: "image_url",
  922. ImageURL: &ChatImageURL{URL: imageURL},
  923. }})
  924. default:
  925. return json.Marshal(rawString(part["text"]))
  926. }
  927. }
  928. // customToolInputSchema 是 custom/freeform 工具降级为 function 工具时的参数 schema。
  929. // chat 协议无法表达 custom 工具的自由文本输入(及其 grammar 约束),退化为单一
  930. // input 字符串参数;回程时再从 arguments 的 input 字段还原(见
  931. // extractCustomToolCallInput)。
  932. const customToolInputSchema = `{"type":"object","properties":{"input":{"type":"string","description":"The raw input for this tool, passed through verbatim."}},"required":["input"]}`
  933. func responsesToolsToChatTools(tools []ResponsesTool) ([]ChatTool, error) {
  934. // 顶层 function/custom 工具名集合:namespace 子工具摊平后与其撞名时,chat
  935. // 上游无法按 namespace 区分调用归属。这类请求在原生 Responses 上游是合法的
  936. // (按 namespace+name 路由),歧义由摊平转换制造且无法消除,必须显式拒绝,
  937. // 不能静默降级(重复声明发给上游、回程还原到错误工具)。
  938. topLevel := make(map[string]bool)
  939. for _, tool := range tools {
  940. if (tool.Type == "function" || tool.Type == "custom") && tool.Name != "" {
  941. if topLevel[tool.Name] {
  942. return nil, fmt.Errorf("duplicate top-level executable tool name %q; this upstream cannot disambiguate duplicate names, rename one of the tools", tool.Name)
  943. }
  944. topLevel[tool.Name] = true
  945. }
  946. }
  947. flatOwner := make(map[string]NamespacedToolName)
  948. toolSearchDeclared := false
  949. out := make([]ChatTool, 0, len(tools))
  950. for _, tool := range tools {
  951. switch tool.Type {
  952. case "function":
  953. out = append(out, ChatTool{
  954. Type: "function",
  955. Function: &ChatFunction{
  956. Name: tool.Name,
  957. Description: tool.Description,
  958. Parameters: tool.Parameters,
  959. Strict: tool.Strict,
  960. },
  961. })
  962. case "custom":
  963. // codex 0.14x 的核心执行工具 exec 即为 custom 类型;丢弃它会让模型
  964. // 无法执行任何命令,必须降级为 function 工具透传。
  965. out = append(out, ChatTool{
  966. Type: "function",
  967. Function: &ChatFunction{
  968. Name: tool.Name,
  969. Description: tool.Description,
  970. Parameters: json.RawMessage(customToolInputSchema),
  971. },
  972. })
  973. case "tool_search":
  974. // 代理不能改名(codex 的模型侧按 tool_search 这个名字调用),与客户端
  975. // 声明的同名工具无法区分——回程会把普通工具的调用劫持成 tool_search_call,
  976. // 必须显式拒绝;重复声明 type=tool_search 去重即可。
  977. if topLevel[toolSearchProxyName] {
  978. return nil, fmt.Errorf("built-in tool_search conflicts with a declared tool named %q; this upstream cannot disambiguate them, rename the tool", toolSearchProxyName)
  979. }
  980. if toolSearchDeclared {
  981. continue
  982. }
  983. toolSearchDeclared = true
  984. out = append(out, toolSearchProxyChatTool())
  985. case "namespace":
  986. flattened, err := namespaceChildrenToChatTools(tool, topLevel, flatOwner)
  987. if err != nil {
  988. return nil, err
  989. }
  990. out = append(out, flattened...)
  991. case "x_search":
  992. out = append(out, ChatTool{
  993. Type: "x_search",
  994. AllowedXHandles: tool.AllowedXHandles,
  995. ExcludedXHandles: tool.ExcludedXHandles,
  996. FromDate: tool.FromDate,
  997. ToDate: tool.ToDate,
  998. EnableImageUnderstanding: tool.EnableImageUnderstanding,
  999. EnableVideoUnderstanding: tool.EnableVideoUnderstanding,
  1000. })
  1001. }
  1002. // 其余类型(web_search、image_generation 等服务端工具)在 chat 上游没有
  1003. // 对应能力,维持丢弃。
  1004. }
  1005. return out, nil
  1006. }
  1007. // toolSearchProxyName 是 tool_search 服务端工具降级后的 function 工具名。模型对
  1008. // 它的调用以同名 function_call 原样回传,由 codex 端路由。
  1009. const toolSearchProxyName = "tool_search"
  1010. const toolSearchProxySchema = `{"type":"object","properties":{"query":{"type":"string","description":"Search query for tools or connectors to load."},"limit":{"type":"integer","description":"Maximum number of tool groups to return."}},"required":["query"]}`
  1011. func toolSearchProxyChatTool() ChatTool {
  1012. return ChatTool{
  1013. Type: "function",
  1014. Function: &ChatFunction{
  1015. Name: toolSearchProxyName,
  1016. Description: "Search and load Codex tools, plugins, connectors, and MCP namespaces for the current task.",
  1017. Parameters: json.RawMessage(toolSearchProxySchema),
  1018. },
  1019. }
  1020. }
  1021. // namespaceChildrenToChatTools 将 namespace 工具的子 function 工具摊平为顶层
  1022. // function 工具,名字加 "<namespace>__" 前缀。摊平名与顶层工具或其他 namespace
  1023. // 撞名时返回错误(歧义不可消除,显式拒绝);同一 (namespace, 子工具) 的重复声明
  1024. // 去重后不算冲突。
  1025. func namespaceChildrenToChatTools(tool ResponsesTool, topLevel map[string]bool, flatOwner map[string]NamespacedToolName) ([]ChatTool, error) {
  1026. if tool.Name == "" {
  1027. return nil, nil
  1028. }
  1029. children := tool.Tools
  1030. if len(children) == 0 {
  1031. children = tool.Children
  1032. }
  1033. var out []ChatTool
  1034. for _, child := range children {
  1035. if child.Type != "function" || child.Name == "" {
  1036. continue
  1037. }
  1038. flat := flattenNamespaceToolName(tool.Name, child.Name)
  1039. entry := NamespacedToolName{Namespace: tool.Name, Name: child.Name}
  1040. if topLevel[flat] {
  1041. return nil, fmt.Errorf("namespace tool %q/%q flattens to %q which conflicts with a top-level tool of the same name; this upstream cannot disambiguate them, rename one of the tools", tool.Name, child.Name, flat)
  1042. }
  1043. if prev, ok := flatOwner[flat]; ok {
  1044. if prev == entry {
  1045. continue
  1046. }
  1047. return nil, fmt.Errorf("namespace tools %q/%q and %q/%q both flatten to %q; this upstream cannot disambiguate them, rename one of the tools", prev.Namespace, prev.Name, tool.Name, child.Name, flat)
  1048. }
  1049. flatOwner[flat] = entry
  1050. out = append(out, ChatTool{
  1051. Type: "function",
  1052. Function: &ChatFunction{
  1053. Name: flat,
  1054. Description: child.Description,
  1055. Parameters: child.Parameters,
  1056. Strict: child.Strict,
  1057. },
  1058. })
  1059. }
  1060. return out, nil
  1061. }
  1062. // chatToolNameMaxLen 是 Chat Completions function 工具名的通用长度上限。
  1063. const chatToolNameMaxLen = 64
  1064. // flattenNamespaceToolName 生成 namespace 子工具的摊平名;超长时截断并追加
  1065. // sha256 短哈希保证唯一性。
  1066. func flattenNamespaceToolName(namespace, name string) string {
  1067. full := namespace + "__" + name
  1068. if len(full) <= chatToolNameMaxLen {
  1069. return full
  1070. }
  1071. sum := sha256.Sum256([]byte(full))
  1072. suffix := "__" + hex.EncodeToString(sum[:4])
  1073. prefixLen := chatToolNameMaxLen - len(suffix)
  1074. var prefix strings.Builder
  1075. for _, ch := range full {
  1076. if prefix.Len()+len(string(ch)) > prefixLen {
  1077. break
  1078. }
  1079. _, _ = prefix.WriteRune(ch)
  1080. }
  1081. return prefix.String() + suffix
  1082. }
  1083. // responsesToolChoiceToChatToolChoice 把 Responses 的 tool_choice 转为 chat 形态。
  1084. // declared 是转换后实际声明的 chat 工具名集合:具名选择项仅在目标工具幸存时转发,
  1085. // 服务端工具(web_search 等)的选择项随工具本身丢弃——指向未声明工具的 tool_choice
  1086. // 会被 chat 上游 400 拒绝。返回 nil 表示丢弃 tool_choice。
  1087. func responsesToolChoiceToChatToolChoice(raw json.RawMessage, declared map[string]bool) json.RawMessage {
  1088. var choice map[string]json.RawMessage
  1089. if err := json.Unmarshal(raw, &choice); err != nil {
  1090. // "auto"/"none"/"required" 等字符串形式原样转发。
  1091. return raw
  1092. }
  1093. var name string
  1094. switch rawString(choice["type"]) {
  1095. case "x_search":
  1096. if !declared["x_search"] {
  1097. return nil
  1098. }
  1099. out, err := json.Marshal(map[string]any{"type": "x_search"})
  1100. if err != nil {
  1101. return raw
  1102. }
  1103. return out
  1104. case "tool_search":
  1105. // tool_search 未被丢弃而是降级为同名 function 代理(见
  1106. // responsesToolsToChatTools),强制选择它同样降级为 function 选择,
  1107. // 静默丢弃会把强制搜索退化为自动选择。
  1108. name = toolSearchProxyName
  1109. case "function", "custom":
  1110. // custom 工具已降级为 function 工具,指向它的 tool_choice 同样按 function 转换。
  1111. name = rawString(choice["name"])
  1112. if name == "" {
  1113. name = rawNestedString(choice["function"], "name")
  1114. }
  1115. if name == "" {
  1116. return raw
  1117. }
  1118. default:
  1119. return nil
  1120. }
  1121. if !declared[name] {
  1122. return nil
  1123. }
  1124. out, err := json.Marshal(map[string]any{
  1125. "type": "function",
  1126. "function": map[string]string{
  1127. "name": name,
  1128. },
  1129. })
  1130. if err != nil {
  1131. return raw
  1132. }
  1133. return out
  1134. }
  1135. // extractCustomToolCallInput 从降级 function 调用的 arguments 中还原 custom 工具的
  1136. // 自由文本输入:优先取 {"input": "..."} 的 input 字段;模型未按 schema 输出时原样
  1137. // 回传,交由客户端校验、模型重试。
  1138. func extractCustomToolCallInput(arguments string) string {
  1139. trimmed := strings.TrimSpace(arguments)
  1140. if trimmed == "" {
  1141. return ""
  1142. }
  1143. var obj map[string]json.RawMessage
  1144. if err := json.Unmarshal([]byte(trimmed), &obj); err != nil {
  1145. return trimmed
  1146. }
  1147. if raw, ok := obj["input"]; ok {
  1148. var s string
  1149. if err := json.Unmarshal(raw, &s); err == nil {
  1150. return s
  1151. }
  1152. return trimmed
  1153. }
  1154. if len(obj) == 0 {
  1155. return ""
  1156. }
  1157. return trimmed
  1158. }
  1159. // ChatCompletionsResponseToResponses converts a non-streaming Chat Completions
  1160. // response into a Responses API response. customTools 是客户端请求中 custom 工具
  1161. // 的名字集合(见 CustomToolNames),命中的调用会还原为 custom_tool_call 项;
  1162. // toolSearch 表示客户端声明了 tool_search 工具(见 HasToolSearchTool),代理工具
  1163. // 的调用会还原为 tool_search_call 项;namespaceTools 是 namespace 子工具的摊平名
  1164. // 映射(见 NamespaceToolNames),命中的调用还原为带 namespace 字段的 function_call 项。
  1165. func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model string, customTools, functionTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) *ResponsesResponse {
  1166. id := ""
  1167. if resp != nil {
  1168. id = resp.ID
  1169. }
  1170. if id == "" {
  1171. id = generateResponsesID()
  1172. }
  1173. // Carry the upstream's own creation timestamp when it sent one; otherwise
  1174. // stamp now, same fallback shape as the generated id above.
  1175. createdAt := int64(0)
  1176. if resp != nil {
  1177. createdAt = resp.Created
  1178. }
  1179. if createdAt <= 0 {
  1180. createdAt = time.Now().Unix()
  1181. }
  1182. out := &ResponsesResponse{
  1183. ID: id,
  1184. Object: "response",
  1185. CreatedAt: createdAt,
  1186. Model: model,
  1187. Status: "completed",
  1188. ServiceTier: chatServiceTier(resp),
  1189. }
  1190. if resp == nil {
  1191. out.Output = []ResponsesOutput{emptyResponsesMessageOutput()}
  1192. return out
  1193. }
  1194. if out.Model == "" {
  1195. out.Model = resp.Model
  1196. }
  1197. if len(resp.Choices) > 0 {
  1198. choice := resp.Choices[0]
  1199. out.Output = chatMessageToResponsesOutput(choice.Message, customTools, functionTools, toolSearch, namespaceTools)
  1200. if choice.FinishReason == "length" {
  1201. out.Status = "incomplete"
  1202. out.IncompleteDetails = &ResponsesIncompleteDetails{Reason: "max_output_tokens"}
  1203. }
  1204. }
  1205. if len(out.Output) == 0 {
  1206. out.Output = []ResponsesOutput{emptyResponsesMessageOutput()}
  1207. }
  1208. if resp.Usage != nil {
  1209. out.Usage = ChatUsageToResponsesUsage(resp.Usage)
  1210. }
  1211. return out
  1212. }
  1213. func chatServiceTier(resp *ChatCompletionsResponse) string {
  1214. if resp == nil {
  1215. return ""
  1216. }
  1217. return resp.ServiceTier
  1218. }
  1219. func chatMessageToResponsesOutput(message ChatMessage, customTools, functionTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) []ResponsesOutput {
  1220. var outputs []ResponsesOutput
  1221. reasoning := message.reasoningText()
  1222. if reasoning != "" {
  1223. outputs = append(outputs, ResponsesOutput{
  1224. Type: "reasoning",
  1225. ID: generateItemID(),
  1226. Summary: []ResponsesSummary{{
  1227. Type: "summary_text",
  1228. Text: reasoning,
  1229. }},
  1230. })
  1231. }
  1232. text := chatMessageContentText(message.Content)
  1233. if text == "" && strings.TrimSpace(reasoning) != "" && len(message.ToolCalls) == 0 {
  1234. text = reasoning
  1235. }
  1236. if text != "" || len(message.ToolCalls) == 0 {
  1237. outputs = append(outputs, ResponsesOutput{
  1238. Type: "message",
  1239. ID: generateItemID(),
  1240. Role: "assistant",
  1241. Content: []ResponsesContentPart{{
  1242. Type: "output_text",
  1243. Text: text,
  1244. }},
  1245. Status: "completed",
  1246. })
  1247. }
  1248. for _, toolCall := range message.ToolCalls {
  1249. arguments := toolCall.Function.Arguments
  1250. if strings.TrimSpace(arguments) == "" {
  1251. arguments = "{}"
  1252. }
  1253. if customName, ok := customToolCallName(toolCall.Function.Name, customTools, functionTools, namespaceTools); ok {
  1254. outputs = append(outputs, ResponsesOutput{
  1255. Type: "custom_tool_call",
  1256. ID: generateItemID(),
  1257. CallID: toolCall.ID,
  1258. Name: customName,
  1259. Input: extractCustomToolCallInput(arguments),
  1260. Status: "completed",
  1261. })
  1262. continue
  1263. }
  1264. if toolSearch && toolCall.Function.Name == toolSearchProxyName {
  1265. outputs = append(outputs, ResponsesOutput{
  1266. Type: "tool_search_call",
  1267. ID: generateItemID(),
  1268. CallID: toolCall.ID,
  1269. Arguments: arguments,
  1270. Status: "completed",
  1271. })
  1272. continue
  1273. }
  1274. // Ordinary Responses function_call arguments must contain valid JSON.
  1275. // Do not mark a truncated non-streaming Chat tool call as completed;
  1276. // Codex would persist it and poison the next request in the same way as
  1277. // the streaming variant guarded by ValidateToolCallArguments.
  1278. if !json.Valid([]byte(arguments)) {
  1279. continue
  1280. }
  1281. if ns, ok := namespaceTools[toolCall.Function.Name]; ok {
  1282. outputs = append(outputs, ResponsesOutput{
  1283. Type: "function_call",
  1284. ID: generateItemID(),
  1285. CallID: toolCall.ID,
  1286. Name: ns.Name,
  1287. Namespace: ns.Namespace,
  1288. Arguments: arguments,
  1289. Status: "completed",
  1290. })
  1291. continue
  1292. }
  1293. outputs = append(outputs, ResponsesOutput{
  1294. Type: "function_call",
  1295. ID: generateItemID(),
  1296. CallID: toolCall.ID,
  1297. Name: toolCall.Function.Name,
  1298. Arguments: arguments,
  1299. Status: "completed",
  1300. })
  1301. }
  1302. return outputs
  1303. }
  1304. // toolSearchCallArgumentsJSON 把降级 function 调用累积的 arguments 字符串还原为
  1305. // tool_search_call 线上要求的 JSON 对象;模型未按 schema 输出(非法 JSON)时按
  1306. // 字符串值兜底,交由 codex 解析报错后让模型重试。
  1307. func toolSearchCallArgumentsJSON(arguments string) json.RawMessage {
  1308. trimmed := strings.TrimSpace(arguments)
  1309. if trimmed == "" {
  1310. return json.RawMessage(`{}`)
  1311. }
  1312. if json.Valid([]byte(trimmed)) {
  1313. return json.RawMessage(trimmed)
  1314. }
  1315. fallback, _ := json.Marshal(arguments)
  1316. return fallback
  1317. }
  1318. func emptyResponsesMessageOutput() ResponsesOutput {
  1319. return ResponsesOutput{
  1320. Type: "message",
  1321. ID: generateItemID(),
  1322. Role: "assistant",
  1323. Content: []ResponsesContentPart{{Type: "output_text", Text: ""}},
  1324. Status: "completed",
  1325. }
  1326. }
  1327. func chatMessageContentText(raw json.RawMessage) string {
  1328. raw = bytesTrimSpace(raw)
  1329. if len(raw) == 0 || string(raw) == "null" {
  1330. return ""
  1331. }
  1332. var text string
  1333. if err := json.Unmarshal(raw, &text); err == nil {
  1334. return text
  1335. }
  1336. var parts []ChatContentPart
  1337. if err := json.Unmarshal(raw, &parts); err == nil {
  1338. var texts []string
  1339. for _, part := range parts {
  1340. if part.Type == "text" && part.Text != "" {
  1341. texts = append(texts, part.Text)
  1342. }
  1343. }
  1344. return strings.Join(texts, "\n\n")
  1345. }
  1346. return ""
  1347. }
  1348. // ChatUsageToResponsesUsage converts Chat Completions token usage to Responses
  1349. // usage shape.
  1350. func ChatUsageToResponsesUsage(usage *ChatUsage) *ResponsesUsage {
  1351. if usage == nil {
  1352. return nil
  1353. }
  1354. out := &ResponsesUsage{
  1355. InputTokens: usage.PromptTokens,
  1356. OutputTokens: usage.CompletionTokens,
  1357. TotalTokens: usage.TotalTokens,
  1358. }
  1359. if out.TotalTokens == 0 {
  1360. out.TotalTokens = out.InputTokens + out.OutputTokens
  1361. }
  1362. if usage.PromptTokensDetails != nil && (usage.PromptTokensDetails.CachedTokens > 0 ||
  1363. usage.PromptTokensDetails.CacheCreationTokens > 0 || usage.PromptTokensDetails.CacheWriteTokens > 0) {
  1364. out.InputTokensDetails = &ResponsesInputTokensDetails{
  1365. CachedTokens: usage.PromptTokensDetails.CachedTokens,
  1366. CacheCreationTokens: usage.PromptTokensDetails.CacheCreationTokens,
  1367. CacheWriteTokens: usage.PromptTokensDetails.CacheWriteTokens,
  1368. }
  1369. if usage.PromptTokensDetails.CacheWriteTokens > 0 {
  1370. out.CacheCreationInputTokens = usage.PromptTokensDetails.CacheWriteTokens
  1371. } else {
  1372. out.CacheCreationInputTokens = usage.PromptTokensDetails.CacheCreationTokens
  1373. }
  1374. }
  1375. return out
  1376. }
  1377. // ChatCompletionsToResponsesStreamState tracks state while converting Chat
  1378. // Completions SSE chunks into Responses SSE events.
  1379. type ChatCompletionsToResponsesStreamState struct {
  1380. ResponseID string
  1381. Model string
  1382. Created int64
  1383. ServiceTier string // upstream Chat chunk service_tier, echoed on response events
  1384. SequenceNumber int
  1385. CreatedSent bool
  1386. CompletedSent bool
  1387. // nextOutputIndex assigns sequential output_index values to items as they
  1388. // are opened (reasoning, message, tool calls), so the streamed indices match
  1389. // the order of items in the final response.output array.
  1390. nextOutputIndex int
  1391. // Reasoning item lifecycle. DeepSeek-style upstreams stream all
  1392. // reasoning_content before any content, so reasoning is modeled as its own
  1393. // "reasoning" output item that must be opened (output_item.added) before any
  1394. // reasoning delta and closed before the message/tool items open.
  1395. ReasoningItemID string
  1396. ReasoningIndex int
  1397. ReasoningOpen bool
  1398. ReasoningDone bool
  1399. // Message item + output_text content-part lifecycle.
  1400. MessageItemID string
  1401. MessageIndex int
  1402. TextPartOpen bool
  1403. Text strings.Builder
  1404. Reasoning strings.Builder
  1405. // Tool-call lifecycle, keyed by the upstream tool_call index.
  1406. ToolCalls map[int]*ChatToolCall
  1407. ToolItemIDs map[int]string
  1408. ToolOutputIndex map[int]int
  1409. // CustomTools 是客户端请求中 custom/freeform 工具的名字集合(见
  1410. // CustomToolNames)。命中的调用按 custom_tool_call 生命周期下发,codex 才能
  1411. // 路由回它注册的 custom 工具。
  1412. CustomTools map[string]bool
  1413. // FunctionTools is the set of explicitly declared top-level function tools.
  1414. FunctionTools map[string]bool
  1415. // ToolSearchDeclared 表示客户端请求声明了 tool_search 工具(见
  1416. // HasToolSearchTool)。命中的代理调用按 tool_search_call 项还原,codex 只按
  1417. // 该项类型(且 execution=client)执行 tool search。
  1418. ToolSearchDeclared bool
  1419. // NamespaceTools 是 namespace 子工具的摊平名 → 原始归属映射(见
  1420. // NamespaceToolNames)。命中的调用还原为带 namespace 字段的 function_call 项,
  1421. // codex 按 namespace+name 路由。
  1422. NamespaceTools map[string]NamespacedToolName
  1423. // toolIsCustom 记录每个工具调用宣告时的类型判定,保证 added/done 事件的
  1424. // 项类型一致。
  1425. toolIsCustom map[int]bool
  1426. // toolIsToolSearch 记录工具调用是否判定为 tool_search 代理调用。
  1427. toolIsToolSearch map[int]bool
  1428. // toolNamespace 记录工具调用宣告时命中的 namespace 归属(见 NamespaceTools)。
  1429. toolNamespace map[int]NamespacedToolName
  1430. // toolAnnounced 记录 output_item.added 是否已发出。存在 custom 工具且名字
  1431. // 尚未到达时延迟宣告,待名字可判定类型后再补发(见 announceChatToolItem)。
  1432. toolAnnounced map[int]bool
  1433. FinishReason string
  1434. Usage *ResponsesUsage
  1435. }
  1436. // NewChatCompletionsToResponsesStreamState returns an initialized stream state.
  1437. func NewChatCompletionsToResponsesStreamState(model string) *ChatCompletionsToResponsesStreamState {
  1438. return &ChatCompletionsToResponsesStreamState{
  1439. ResponseID: generateResponsesID(),
  1440. Model: model,
  1441. Created: time.Now().Unix(),
  1442. ToolCalls: make(map[int]*ChatToolCall),
  1443. ToolItemIDs: make(map[int]string),
  1444. ToolOutputIndex: make(map[int]int),
  1445. toolIsCustom: make(map[int]bool),
  1446. toolIsToolSearch: make(map[int]bool),
  1447. toolNamespace: make(map[int]NamespacedToolName),
  1448. toolAnnounced: make(map[int]bool),
  1449. }
  1450. }
  1451. // ValidateToolCallArguments checks the accumulated function-call arguments
  1452. // before the stream is finalized. A tool call whose argument stream was
  1453. // truncated must not be emitted as a completed Responses item: Codex will
  1454. // persist it and replay it on the next turn, where a Chat Completions provider
  1455. // rejects the whole request.
  1456. func (state *ChatCompletionsToResponsesStreamState) ValidateToolCallArguments() error {
  1457. if state == nil {
  1458. return nil
  1459. }
  1460. for idx, toolCall := range state.ToolCalls {
  1461. if toolCall == nil {
  1462. continue
  1463. }
  1464. if state.toolIsCustom[idx] || state.toolIsToolSearch[idx] {
  1465. continue
  1466. }
  1467. arguments := strings.TrimSpace(toolCall.Function.Arguments)
  1468. if arguments == "" {
  1469. continue
  1470. }
  1471. if !json.Valid([]byte(arguments)) {
  1472. return fmt.Errorf("tool call %q (%s) arguments are invalid JSON", toolCall.ID, toolCall.Function.Name)
  1473. }
  1474. }
  1475. return nil
  1476. }
  1477. func (state *ChatCompletionsToResponsesStreamState) allocOutputIndex() int {
  1478. idx := state.nextOutputIndex
  1479. state.nextOutputIndex++
  1480. return idx
  1481. }
  1482. // ChatCompletionsChunkToResponsesEvents converts one Chat Completions stream
  1483. // chunk into zero or more Responses stream events.
  1484. func ChatCompletionsChunkToResponsesEvents(
  1485. chunk *ChatCompletionsChunk,
  1486. state *ChatCompletionsToResponsesStreamState,
  1487. ) []ResponsesStreamEvent {
  1488. if chunk == nil || state == nil {
  1489. return nil
  1490. }
  1491. if chunk.ID != "" {
  1492. state.ResponseID = chunk.ID
  1493. }
  1494. if state.Model == "" && chunk.Model != "" {
  1495. state.Model = chunk.Model
  1496. }
  1497. if chunk.ServiceTier != "" {
  1498. state.ServiceTier = chunk.ServiceTier
  1499. }
  1500. if chunk.Usage != nil {
  1501. state.Usage = ChatUsageToResponsesUsage(chunk.Usage)
  1502. }
  1503. var events []ResponsesStreamEvent
  1504. events = append(events, ensureChatToResponsesCreated(state)...)
  1505. for _, choice := range chunk.Choices {
  1506. // Reasoning is emitted as its own output item and must be opened
  1507. // (output_item.added + reasoning_summary_part.added) before the first
  1508. // delta, otherwise a strict client discards the delta. The leading
  1509. // empty-string reasoning delta upstreams send is filtered out.
  1510. reasoning := choice.Delta.reasoningText()
  1511. if reasoning != nil && *reasoning != "" {
  1512. events = append(events, ensureChatReasoningItem(state)...)
  1513. _, _ = state.Reasoning.WriteString(*reasoning)
  1514. events = append(events, chatToResponsesEvent(state, "response.reasoning_summary_text.delta", &ResponsesStreamEvent{
  1515. OutputIndex: state.ReasoningIndex,
  1516. SummaryIndex: 0,
  1517. Delta: *reasoning,
  1518. ItemID: state.ReasoningItemID,
  1519. }))
  1520. }
  1521. if choice.Delta.Content != nil && *choice.Delta.Content != "" {
  1522. // First real content closes the reasoning item, then opens the
  1523. // message item and its output_text content part.
  1524. events = append(events, closeChatReasoningItem(state)...)
  1525. events = append(events, ensureChatToResponsesMessageItem(state)...)
  1526. events = append(events, ensureChatToResponsesTextPart(state)...)
  1527. _, _ = state.Text.WriteString(*choice.Delta.Content)
  1528. events = append(events, chatToResponsesEvent(state, "response.output_text.delta", &ResponsesStreamEvent{
  1529. OutputIndex: state.MessageIndex,
  1530. ContentIndex: 0,
  1531. Delta: *choice.Delta.Content,
  1532. ItemID: state.MessageItemID,
  1533. }))
  1534. }
  1535. for _, toolCall := range choice.Delta.ToolCalls {
  1536. idx := 0
  1537. if toolCall.Index != nil {
  1538. idx = *toolCall.Index
  1539. }
  1540. stored, ok := state.ToolCalls[idx]
  1541. if !ok {
  1542. // A tool call closes any open reasoning item first.
  1543. events = append(events, closeChatReasoningItem(state)...)
  1544. copyCall := toolCall
  1545. if copyCall.ID == "" {
  1546. copyCall.ID = generateItemID()
  1547. }
  1548. copyCall.Type = "function"
  1549. // Arguments are accumulated by the shared block below so the
  1550. // emitted delta and the stored value stay in sync. Some upstreams
  1551. // (e.g. GLM/Zhipu) pack id+name+arguments into the first tool_call
  1552. // chunk; without this reset the first chunk's arguments would be
  1553. // counted twice (once from this copy, once from the += below),
  1554. // producing a doubled, invalid JSON like {"a":1}{"a":1}.
  1555. copyCall.Function.Arguments = ""
  1556. state.ToolCalls[idx] = &copyCall
  1557. stored = &copyCall
  1558. state.ToolItemIDs[idx] = generateItemID()
  1559. state.ToolOutputIndex[idx] = state.allocOutputIndex()
  1560. } else {
  1561. if toolCall.ID != "" {
  1562. stored.ID = toolCall.ID
  1563. }
  1564. if toolCall.Function.Name != "" {
  1565. stored.Function.Name = toolCall.Function.Name
  1566. }
  1567. }
  1568. events = append(events, announceChatToolItem(state, idx, stored, false)...)
  1569. if toolCall.Function.Arguments != "" {
  1570. stored.Function.Arguments += toolCall.Function.Arguments
  1571. // 未宣告(名字未到)时仅累积,宣告时统一补发;custom 调用的
  1572. // arguments 是包裹 input 的 JSON 片段,无法增量还原为自由文本
  1573. // 输入,缓冲整份 arguments 收尾时一次性下发(见 closeChatToolItems);
  1574. // tool_search 调用同样收尾时随 output_item.done 全量下发。
  1575. if state.toolAnnounced[idx] && !state.toolIsCustom[idx] && !state.toolIsToolSearch[idx] {
  1576. events = append(events, chatToResponsesEvent(state, "response.function_call_arguments.delta", &ResponsesStreamEvent{
  1577. OutputIndex: state.ToolOutputIndex[idx],
  1578. ItemID: state.ToolItemIDs[idx],
  1579. Delta: toolCall.Function.Arguments,
  1580. CallID: stored.ID,
  1581. Name: stored.Function.Name,
  1582. }))
  1583. }
  1584. }
  1585. }
  1586. if choice.FinishReason != nil && *choice.FinishReason != "" {
  1587. state.FinishReason = *choice.FinishReason
  1588. }
  1589. }
  1590. return events
  1591. }
  1592. // FinalizeChatCompletionsResponsesStream emits terminal Responses events.
  1593. func FinalizeChatCompletionsResponsesStream(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1594. if state == nil || state.CompletedSent {
  1595. return nil
  1596. }
  1597. var events []ResponsesStreamEvent
  1598. events = append(events, ensureChatToResponsesCreated(state)...)
  1599. // Close a reasoning item that never transitioned to content (reasoning-only
  1600. // or empty completion).
  1601. events = append(events, closeChatReasoningItem(state)...)
  1602. events = append(events, synthesizeChatReasoningFallbackMessage(state)...)
  1603. if state.MessageItemID != "" {
  1604. if state.TextPartOpen {
  1605. events = append(events, chatToResponsesEvent(state, "response.output_text.done", &ResponsesStreamEvent{
  1606. OutputIndex: state.MessageIndex,
  1607. ContentIndex: 0,
  1608. Text: state.Text.String(),
  1609. ItemID: state.MessageItemID,
  1610. }))
  1611. events = append(events, chatToResponsesEvent(state, "response.content_part.done", &ResponsesStreamEvent{
  1612. OutputIndex: state.MessageIndex,
  1613. ContentIndex: 0,
  1614. ItemID: state.MessageItemID,
  1615. Part: &ResponsesContentPart{Type: "output_text", Text: state.Text.String()},
  1616. }))
  1617. }
  1618. events = append(events, chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{
  1619. OutputIndex: state.MessageIndex,
  1620. Item: &ResponsesOutput{
  1621. Type: "message",
  1622. ID: state.MessageItemID,
  1623. Role: "assistant",
  1624. Content: []ResponsesContentPart{{Type: "output_text", Text: state.Text.String()}},
  1625. Status: "completed",
  1626. },
  1627. }))
  1628. }
  1629. // Close every function_call item opened during the stream. Codex finalizes a
  1630. // tool call only after function_call_arguments.done + output_item.done for
  1631. // that item; without them the call never completes and the session wedges.
  1632. // Mirrors cc-switch's finalize_tools.
  1633. events = append(events, closeChatToolItems(state)...)
  1634. status := "completed"
  1635. var incompleteDetails *ResponsesIncompleteDetails
  1636. if state.FinishReason == "length" {
  1637. status = "incomplete"
  1638. incompleteDetails = &ResponsesIncompleteDetails{Reason: "max_output_tokens"}
  1639. }
  1640. state.CompletedSent = true
  1641. events = append(events, chatToResponsesEvent(state, "response.completed", &ResponsesStreamEvent{
  1642. Response: &ResponsesResponse{
  1643. ID: state.ResponseID,
  1644. Object: "response",
  1645. CreatedAt: state.Created,
  1646. Model: state.Model,
  1647. Status: status,
  1648. ServiceTier: state.ServiceTier,
  1649. Output: state.chatOutput(),
  1650. Usage: state.Usage,
  1651. IncompleteDetails: incompleteDetails,
  1652. },
  1653. }))
  1654. return events
  1655. }
  1656. func ensureChatToResponsesCreated(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1657. if state.CreatedSent {
  1658. return nil
  1659. }
  1660. state.CreatedSent = true
  1661. return []ResponsesStreamEvent{chatToResponsesEvent(state, "response.created", &ResponsesStreamEvent{
  1662. Response: &ResponsesResponse{
  1663. ID: state.ResponseID,
  1664. Object: "response",
  1665. CreatedAt: state.Created,
  1666. Model: state.Model,
  1667. Status: "in_progress",
  1668. ServiceTier: state.ServiceTier,
  1669. Output: []ResponsesOutput{},
  1670. },
  1671. })}
  1672. }
  1673. // ensureChatReasoningItem opens the reasoning output item (output_item.added +
  1674. // reasoning_summary_part.added) before the first reasoning delta. Codex renders
  1675. // streaming reasoning only when this summary-part lifecycle is present.
  1676. func ensureChatReasoningItem(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1677. if state.ReasoningOpen || state.ReasoningDone {
  1678. return nil
  1679. }
  1680. state.ReasoningOpen = true
  1681. state.ReasoningItemID = generateItemID()
  1682. state.ReasoningIndex = state.allocOutputIndex()
  1683. return []ResponsesStreamEvent{
  1684. chatToResponsesEvent(state, "response.output_item.added", &ResponsesStreamEvent{
  1685. OutputIndex: state.ReasoningIndex,
  1686. Item: &ResponsesOutput{Type: "reasoning", ID: state.ReasoningItemID, Status: "in_progress"},
  1687. }),
  1688. chatToResponsesEvent(state, "response.reasoning_summary_part.added", &ResponsesStreamEvent{
  1689. OutputIndex: state.ReasoningIndex,
  1690. SummaryIndex: 0,
  1691. ItemID: state.ReasoningItemID,
  1692. Part: &ResponsesContentPart{Type: "summary_text"},
  1693. }),
  1694. }
  1695. }
  1696. // closeChatReasoningItem emits the reasoning item's terminal events
  1697. // (reasoning_summary_text.done + reasoning_summary_part.done + output_item.done).
  1698. func closeChatReasoningItem(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1699. if !state.ReasoningOpen {
  1700. return nil
  1701. }
  1702. state.ReasoningOpen = false
  1703. state.ReasoningDone = true
  1704. reasoning := state.Reasoning.String()
  1705. return []ResponsesStreamEvent{
  1706. chatToResponsesEvent(state, "response.reasoning_summary_text.done", &ResponsesStreamEvent{
  1707. OutputIndex: state.ReasoningIndex,
  1708. SummaryIndex: 0,
  1709. Text: reasoning,
  1710. ItemID: state.ReasoningItemID,
  1711. }),
  1712. chatToResponsesEvent(state, "response.reasoning_summary_part.done", &ResponsesStreamEvent{
  1713. OutputIndex: state.ReasoningIndex,
  1714. SummaryIndex: 0,
  1715. ItemID: state.ReasoningItemID,
  1716. Part: &ResponsesContentPart{Type: "summary_text", Text: reasoning},
  1717. }),
  1718. chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{
  1719. OutputIndex: state.ReasoningIndex,
  1720. Item: &ResponsesOutput{
  1721. Type: "reasoning",
  1722. ID: state.ReasoningItemID,
  1723. Status: "completed",
  1724. Summary: []ResponsesSummary{{Type: "summary_text", Text: reasoning}},
  1725. },
  1726. }),
  1727. }
  1728. }
  1729. func synthesizeChatReasoningFallbackMessage(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1730. if state == nil ||
  1731. state.MessageItemID != "" ||
  1732. state.Text.Len() > 0 ||
  1733. state.Reasoning.Len() == 0 ||
  1734. len(state.ToolCalls) > 0 {
  1735. return nil
  1736. }
  1737. text := state.Reasoning.String()
  1738. if strings.TrimSpace(text) == "" {
  1739. return nil
  1740. }
  1741. var events []ResponsesStreamEvent
  1742. events = append(events, ensureChatToResponsesMessageItem(state)...)
  1743. events = append(events, ensureChatToResponsesTextPart(state)...)
  1744. _, _ = state.Text.WriteString(text)
  1745. events = append(events, chatToResponsesEvent(state, "response.output_text.delta", &ResponsesStreamEvent{
  1746. OutputIndex: state.MessageIndex,
  1747. ContentIndex: 0,
  1748. Delta: text,
  1749. ItemID: state.MessageItemID,
  1750. }))
  1751. return events
  1752. }
  1753. func ensureChatToResponsesMessageItem(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1754. if state.MessageItemID != "" {
  1755. return nil
  1756. }
  1757. state.MessageItemID = generateItemID()
  1758. state.MessageIndex = state.allocOutputIndex()
  1759. return []ResponsesStreamEvent{chatToResponsesEvent(state, "response.output_item.added", &ResponsesStreamEvent{
  1760. OutputIndex: state.MessageIndex,
  1761. Item: &ResponsesOutput{
  1762. Type: "message",
  1763. ID: state.MessageItemID,
  1764. Role: "assistant",
  1765. Status: "in_progress",
  1766. Content: []ResponsesContentPart{{Type: "output_text"}},
  1767. },
  1768. })}
  1769. }
  1770. func ensureChatToResponsesTextPart(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1771. if state.TextPartOpen {
  1772. return nil
  1773. }
  1774. state.TextPartOpen = true
  1775. return []ResponsesStreamEvent{chatToResponsesEvent(state, "response.content_part.added", &ResponsesStreamEvent{
  1776. OutputIndex: state.MessageIndex,
  1777. ContentIndex: 0,
  1778. ItemID: state.MessageItemID,
  1779. Part: &ResponsesContentPart{Type: "output_text", Text: ""},
  1780. })}
  1781. }
  1782. // announceChatToolItem 在类型可判定时发出工具调用的 output_item.added。custom
  1783. // 工具的判定依赖名字:名字未到且请求里存在 custom 工具时延迟宣告,避免 added/done
  1784. // 的项类型不一致;force 用于流收尾,名字始终未到时按 function_call 兜底。
  1785. func announceChatToolItem(
  1786. state *ChatCompletionsToResponsesStreamState,
  1787. idx int,
  1788. stored *ChatToolCall,
  1789. force bool,
  1790. ) []ResponsesStreamEvent {
  1791. if state.toolAnnounced[idx] {
  1792. return nil
  1793. }
  1794. if !force && stored.Function.Name == "" && (len(state.CustomTools) > 0 || len(state.FunctionTools) > 0 || state.ToolSearchDeclared || len(state.NamespaceTools) > 0) {
  1795. return nil
  1796. }
  1797. state.toolAnnounced[idx] = true
  1798. customName, isCustom := customToolCallName(stored.Function.Name, state.CustomTools, state.FunctionTools, state.NamespaceTools)
  1799. isToolSearch := !isCustom && state.ToolSearchDeclared && stored.Function.Name == toolSearchProxyName
  1800. state.toolIsCustom[idx] = isCustom
  1801. state.toolIsToolSearch[idx] = isToolSearch
  1802. itemType := "function_call"
  1803. if isCustom {
  1804. itemType = "custom_tool_call"
  1805. }
  1806. if isToolSearch {
  1807. itemType = "tool_search_call"
  1808. }
  1809. // namespace 子工具的调用仍按 function_call 生命周期下发,但 added/done 项要
  1810. // 还原为裸子工具名 + namespace 字段(codex 按 namespace+name 路由)。
  1811. itemName, itemNamespace := stored.Function.Name, ""
  1812. if isCustom {
  1813. itemName = customName
  1814. }
  1815. if ns, ok := state.NamespaceTools[stored.Function.Name]; ok && !isCustom && !isToolSearch {
  1816. state.toolNamespace[idx] = ns
  1817. itemName, itemNamespace = ns.Name, ns.Namespace
  1818. }
  1819. events := []ResponsesStreamEvent{chatToResponsesEvent(state, "response.output_item.added", &ResponsesStreamEvent{
  1820. OutputIndex: state.ToolOutputIndex[idx],
  1821. Item: &ResponsesOutput{
  1822. Type: itemType,
  1823. ID: state.ToolItemIDs[idx],
  1824. CallID: stored.ID,
  1825. Name: itemName,
  1826. Namespace: itemNamespace,
  1827. Status: "in_progress",
  1828. },
  1829. })}
  1830. // 迟到宣告时补发已累积的参数增量(custom/tool_search 的输入收尾统一下发,不补发)。
  1831. if !isCustom && !isToolSearch && stored.Function.Arguments != "" {
  1832. events = append(events, chatToResponsesEvent(state, "response.function_call_arguments.delta", &ResponsesStreamEvent{
  1833. OutputIndex: state.ToolOutputIndex[idx],
  1834. ItemID: state.ToolItemIDs[idx],
  1835. Delta: stored.Function.Arguments,
  1836. CallID: stored.ID,
  1837. Name: stored.Function.Name,
  1838. }))
  1839. }
  1840. return events
  1841. }
  1842. // closeChatToolItems emits function_call_arguments.done + output_item.done for
  1843. // every tool call opened during the stream, carrying the full call_id/name/
  1844. // arguments so codex can deserialize and execute the call. Mirrors cc-switch's
  1845. // finalize_tools.
  1846. func closeChatToolItems(state *ChatCompletionsToResponsesStreamState) []ResponsesStreamEvent {
  1847. if len(state.ToolCalls) == 0 {
  1848. return nil
  1849. }
  1850. var events []ResponsesStreamEvent
  1851. for i := 0; i < len(state.ToolCalls); i++ {
  1852. toolCall, ok := state.ToolCalls[i]
  1853. if !ok || toolCall == nil {
  1854. continue
  1855. }
  1856. itemID, opened := state.ToolItemIDs[i]
  1857. if !opened {
  1858. continue
  1859. }
  1860. // 名字始终未到导致尚未宣告的调用,收尾前按最终名字兜底宣告。
  1861. events = append(events, announceChatToolItem(state, i, toolCall, true)...)
  1862. arguments := toolCall.Function.Arguments
  1863. if strings.TrimSpace(arguments) == "" {
  1864. arguments = "{}"
  1865. }
  1866. outputIndex := state.ToolOutputIndex[i]
  1867. if state.toolIsCustom[i] {
  1868. // custom 调用按 custom_tool_call 生命周期收尾:input 在此处一次性下发
  1869. // (流中不产出增量,见 ChatCompletionsChunkToResponsesEvents)。
  1870. input := extractCustomToolCallInput(arguments)
  1871. if input != "" {
  1872. events = append(events, chatToResponsesEvent(state, "response.custom_tool_call_input.delta", &ResponsesStreamEvent{
  1873. OutputIndex: outputIndex,
  1874. ItemID: itemID,
  1875. Delta: input,
  1876. }))
  1877. }
  1878. events = append(events,
  1879. chatToResponsesEvent(state, "response.custom_tool_call_input.done", &ResponsesStreamEvent{
  1880. OutputIndex: outputIndex,
  1881. ItemID: itemID,
  1882. CallID: toolCall.ID,
  1883. Name: customNameForStreamTool(state, toolCall.Function.Name),
  1884. Input: input,
  1885. }),
  1886. chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{
  1887. OutputIndex: outputIndex,
  1888. Item: &ResponsesOutput{
  1889. Type: "custom_tool_call",
  1890. ID: itemID,
  1891. CallID: toolCall.ID,
  1892. Name: customNameForStreamTool(state, toolCall.Function.Name),
  1893. Input: input,
  1894. Status: "completed",
  1895. },
  1896. }),
  1897. )
  1898. continue
  1899. }
  1900. if state.toolIsToolSearch[i] {
  1901. // tool_search 调用按 tool_search_call 项收尾:codex 从 output_item.done
  1902. // 物化该调用(无参数增量事件),arguments 全量随项下发。
  1903. events = append(events, chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{
  1904. OutputIndex: outputIndex,
  1905. Item: &ResponsesOutput{
  1906. Type: "tool_search_call",
  1907. ID: itemID,
  1908. CallID: toolCall.ID,
  1909. Arguments: arguments,
  1910. Status: "completed",
  1911. },
  1912. }))
  1913. continue
  1914. }
  1915. // namespace 子工具调用在宣告时已记录归属,收尾项同样带还原名与 namespace。
  1916. name, namespace := toolCall.Function.Name, ""
  1917. if ns, ok := state.toolNamespace[i]; ok {
  1918. name, namespace = ns.Name, ns.Namespace
  1919. }
  1920. events = append(events,
  1921. chatToResponsesEvent(state, "response.function_call_arguments.done", &ResponsesStreamEvent{
  1922. OutputIndex: outputIndex,
  1923. ItemID: itemID,
  1924. CallID: toolCall.ID,
  1925. Name: name,
  1926. Arguments: arguments,
  1927. }),
  1928. chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{
  1929. OutputIndex: outputIndex,
  1930. Item: &ResponsesOutput{
  1931. Type: "function_call",
  1932. ID: itemID,
  1933. CallID: toolCall.ID,
  1934. Name: name,
  1935. Namespace: namespace,
  1936. Arguments: arguments,
  1937. Status: "completed",
  1938. },
  1939. }),
  1940. )
  1941. }
  1942. return events
  1943. }
  1944. func (state *ChatCompletionsToResponsesStreamState) chatOutput() []ResponsesOutput {
  1945. var outputs []ResponsesOutput
  1946. if state.Reasoning.Len() > 0 {
  1947. outputs = append(outputs, ResponsesOutput{
  1948. Type: "reasoning",
  1949. ID: generateItemID(),
  1950. Summary: []ResponsesSummary{{
  1951. Type: "summary_text",
  1952. Text: state.Reasoning.String(),
  1953. }},
  1954. })
  1955. }
  1956. if state.MessageItemID != "" || len(state.ToolCalls) == 0 {
  1957. outputs = append(outputs, ResponsesOutput{
  1958. Type: "message",
  1959. ID: nonEmpty(state.MessageItemID, generateItemID()),
  1960. Role: "assistant",
  1961. Content: []ResponsesContentPart{{
  1962. Type: "output_text",
  1963. Text: state.Text.String(),
  1964. }},
  1965. Status: "completed",
  1966. })
  1967. }
  1968. for i := 0; i < len(state.ToolCalls); i++ {
  1969. toolCall, ok := state.ToolCalls[i]
  1970. if !ok || toolCall == nil {
  1971. continue
  1972. }
  1973. arguments := toolCall.Function.Arguments
  1974. if strings.TrimSpace(arguments) == "" {
  1975. arguments = "{}"
  1976. }
  1977. if state.toolIsCustom[i] {
  1978. outputs = append(outputs, ResponsesOutput{
  1979. Type: "custom_tool_call",
  1980. ID: generateItemID(),
  1981. CallID: toolCall.ID,
  1982. Name: customNameForStreamTool(state, toolCall.Function.Name),
  1983. Input: extractCustomToolCallInput(arguments),
  1984. Status: "completed",
  1985. })
  1986. continue
  1987. }
  1988. if state.toolIsToolSearch[i] {
  1989. outputs = append(outputs, ResponsesOutput{
  1990. Type: "tool_search_call",
  1991. ID: generateItemID(),
  1992. CallID: toolCall.ID,
  1993. Arguments: arguments,
  1994. Status: "completed",
  1995. })
  1996. continue
  1997. }
  1998. name, namespace := toolCall.Function.Name, ""
  1999. if ns, ok := state.toolNamespace[i]; ok {
  2000. name, namespace = ns.Name, ns.Namespace
  2001. }
  2002. outputs = append(outputs, ResponsesOutput{
  2003. Type: "function_call",
  2004. ID: generateItemID(),
  2005. CallID: toolCall.ID,
  2006. Name: name,
  2007. Namespace: namespace,
  2008. Arguments: arguments,
  2009. Status: "completed",
  2010. })
  2011. }
  2012. return outputs
  2013. }
  2014. func chatToResponsesEvent(
  2015. state *ChatCompletionsToResponsesStreamState,
  2016. eventType string,
  2017. template *ResponsesStreamEvent,
  2018. ) ResponsesStreamEvent {
  2019. seq := state.SequenceNumber
  2020. state.SequenceNumber++
  2021. evt := *template
  2022. evt.Type = eventType
  2023. evt.SequenceNumber = seq
  2024. return evt
  2025. }
  2026. func rawString(raw json.RawMessage) string {
  2027. raw = bytesTrimSpace(raw)
  2028. if len(raw) == 0 || string(raw) == "null" {
  2029. return ""
  2030. }
  2031. var s string
  2032. if err := json.Unmarshal(raw, &s); err == nil {
  2033. return s
  2034. }
  2035. return ""
  2036. }
  2037. func rawNestedString(raw json.RawMessage, key string) string {
  2038. var obj map[string]json.RawMessage
  2039. if err := json.Unmarshal(raw, &obj); err != nil {
  2040. return ""
  2041. }
  2042. return rawString(obj[key])
  2043. }
  2044. func bytesTrimSpace(raw json.RawMessage) json.RawMessage {
  2045. return json.RawMessage(strings.TrimSpace(string(raw)))
  2046. }
  2047. func nonEmpty(value, fallback string) string {
  2048. if value != "" {
  2049. return value
  2050. }
  2051. return fallback
  2052. }