responses_to_chatcompletions.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. package apicompat
  2. import (
  3. "crypto/rand"
  4. "encoding/hex"
  5. "encoding/json"
  6. "fmt"
  7. "strings"
  8. "time"
  9. )
  10. // ---------------------------------------------------------------------------
  11. // Non-streaming: ResponsesResponse → ChatCompletionsResponse
  12. // ---------------------------------------------------------------------------
  13. // ResponsesToChatCompletions converts a Responses API response into a Chat
  14. // Completions response. Text output items are concatenated into
  15. // choices[0].message.content; function_call items become tool_calls.
  16. func ResponsesToChatCompletions(resp *ResponsesResponse, model string) *ChatCompletionsResponse {
  17. id := resp.ID
  18. if id == "" {
  19. id = generateChatCmplID()
  20. }
  21. out := &ChatCompletionsResponse{
  22. ID: id,
  23. Object: "chat.completion",
  24. Created: time.Now().Unix(),
  25. Model: model,
  26. ServiceTier: resp.ServiceTier,
  27. }
  28. var contentText string
  29. var reasoningText string
  30. var toolCalls []ChatToolCall
  31. for _, item := range resp.Output {
  32. switch item.Type {
  33. case "message":
  34. for _, part := range item.Content {
  35. if part.Type == "output_text" && part.Text != "" {
  36. contentText += part.Text
  37. }
  38. }
  39. case "function_call":
  40. toolCalls = append(toolCalls, ChatToolCall{
  41. ID: item.CallID,
  42. Type: "function",
  43. Function: ChatFunctionCall{
  44. Name: item.Name,
  45. Arguments: item.Arguments,
  46. },
  47. })
  48. case "reasoning":
  49. for _, s := range item.Summary {
  50. if s.Type == "summary_text" && s.Text != "" {
  51. reasoningText += s.Text
  52. }
  53. }
  54. case "web_search_call":
  55. // silently consumed — results already incorporated into text output
  56. }
  57. }
  58. msg := ChatMessage{Role: "assistant"}
  59. if len(toolCalls) > 0 {
  60. msg.ToolCalls = toolCalls
  61. }
  62. if contentText != "" {
  63. raw, _ := json.Marshal(contentText)
  64. msg.Content = raw
  65. }
  66. if reasoningText != "" {
  67. msg.ReasoningContent = reasoningText
  68. }
  69. finishReason := responsesStatusToChatFinishReason(resp.Status, resp.IncompleteDetails, toolCalls)
  70. out.Choices = []ChatChoice{{
  71. Index: 0,
  72. Message: msg,
  73. FinishReason: finishReason,
  74. }}
  75. out.Usage = chatUsageFromResponsesUsage(resp.Usage)
  76. return out
  77. }
  78. func responsesStatusToChatFinishReason(status string, details *ResponsesIncompleteDetails, toolCalls []ChatToolCall) string {
  79. switch status {
  80. case "incomplete":
  81. if details != nil {
  82. switch details.Reason {
  83. case "max_output_tokens":
  84. return "length"
  85. case "content_filter":
  86. return "content_filter"
  87. }
  88. }
  89. return "stop"
  90. case "completed":
  91. if len(toolCalls) > 0 {
  92. return "tool_calls"
  93. }
  94. return "stop"
  95. default:
  96. return "stop"
  97. }
  98. }
  99. // ---------------------------------------------------------------------------
  100. // Streaming: ResponsesStreamEvent → []ChatCompletionsChunk (stateful converter)
  101. // ---------------------------------------------------------------------------
  102. // ResponsesEventToChatState tracks state for converting a sequence of Responses
  103. // SSE events into Chat Completions SSE chunks.
  104. type ResponsesEventToChatState struct {
  105. ID string
  106. Model string
  107. Created int64
  108. ServiceTier string // upstream tier observed on response events; echoed on chunks
  109. SentRole bool
  110. SawToolCall bool
  111. SawText bool
  112. Finalized bool // true after finish chunk has been emitted
  113. NextToolCallIndex int // next sequential tool_call index to assign
  114. OutputIndexToToolIndex map[int]int // Responses output_index → Chat tool_calls index
  115. OutputIndexToArguments map[int]string
  116. IncludeUsage bool
  117. Usage *ChatUsage
  118. }
  119. // NewResponsesEventToChatState returns an initialised stream state.
  120. func NewResponsesEventToChatState() *ResponsesEventToChatState {
  121. return &ResponsesEventToChatState{
  122. ID: generateChatCmplID(),
  123. Created: time.Now().Unix(),
  124. OutputIndexToToolIndex: make(map[int]int),
  125. OutputIndexToArguments: make(map[int]string),
  126. }
  127. }
  128. // ResponsesEventToChatChunks converts a single Responses SSE event into zero
  129. // or more Chat Completions chunks, updating state as it goes.
  130. func ResponsesEventToChatChunks(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  131. switch evt.Type {
  132. case "response.created":
  133. return resToChatHandleCreated(evt, state)
  134. case "response.output_text.delta":
  135. return resToChatHandleTextDelta(evt, state)
  136. case "response.output_item.added":
  137. return resToChatHandleOutputItemAdded(evt, state)
  138. case "response.function_call_arguments.delta",
  139. // custom/freeform 工具(如新版 apply_patch)的输入增量与 function_call 参数增量同形,
  140. // 均按 OutputIndex 累加到对应工具调用。
  141. "response.custom_tool_call_input.delta":
  142. return resToChatHandleFuncArgsDelta(evt, state)
  143. case "response.function_call_arguments.done", "response.custom_tool_call_input.done":
  144. return resToChatHandleFuncArgsDone(evt, state)
  145. case "response.reasoning_summary_text.delta",
  146. // 原始推理文本增量(真实 Codex 客户端消费的 reasoning_text.delta),
  147. // 与 reasoning summary 一样映射为 reasoning_content。
  148. "response.reasoning_text.delta":
  149. return resToChatHandleReasoningDelta(evt, state)
  150. case "response.reasoning_summary_text.done":
  151. return nil
  152. // response.done 是 Realtime/WS 与项目透传路径使用的终止别名;
  153. // 普通 Responses HTTP SSE 的公开终止事件仍以 response.completed 为主。
  154. case "response.completed", "response.done", "response.incomplete", "response.failed":
  155. return resToChatHandleCompleted(evt, state)
  156. default:
  157. return nil
  158. }
  159. }
  160. // FinalizeResponsesChatStream emits a final chunk with finish_reason if the
  161. // stream ended without a proper completion event (e.g. upstream disconnect).
  162. // It is idempotent: if a completion event already emitted the finish chunk,
  163. // this returns nil.
  164. func FinalizeResponsesChatStream(state *ResponsesEventToChatState) []ChatCompletionsChunk {
  165. if state.Finalized {
  166. return nil
  167. }
  168. state.Finalized = true
  169. finishReason := "stop"
  170. if state.SawToolCall {
  171. finishReason = "tool_calls"
  172. }
  173. chunks := []ChatCompletionsChunk{makeChatFinishChunk(state, finishReason)}
  174. if state.IncludeUsage && state.Usage != nil {
  175. chunks = append(chunks, ChatCompletionsChunk{
  176. ID: state.ID,
  177. Object: "chat.completion.chunk",
  178. Created: state.Created,
  179. Model: state.Model,
  180. ServiceTier: state.ServiceTier,
  181. Choices: []ChatChunkChoice{},
  182. Usage: state.Usage,
  183. })
  184. }
  185. return chunks
  186. }
  187. // ChatChunkToSSE formats a ChatCompletionsChunk as an SSE data line.
  188. func ChatChunkToSSE(chunk ChatCompletionsChunk) (string, error) {
  189. data, err := json.Marshal(chunk)
  190. if err != nil {
  191. return "", err
  192. }
  193. return fmt.Sprintf("data: %s\n\n", data), nil
  194. }
  195. // --- internal handlers ---
  196. func resToChatHandleCreated(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  197. if evt.Response != nil {
  198. if evt.Response.ID != "" {
  199. state.ID = evt.Response.ID
  200. }
  201. if state.Model == "" && evt.Response.Model != "" {
  202. state.Model = evt.Response.Model
  203. }
  204. if evt.Response.ServiceTier != "" {
  205. state.ServiceTier = evt.Response.ServiceTier
  206. }
  207. }
  208. // Emit the role chunk.
  209. if state.SentRole {
  210. return nil
  211. }
  212. state.SentRole = true
  213. role := "assistant"
  214. return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{Role: role})}
  215. }
  216. func resToChatHandleTextDelta(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  217. if evt.Delta == "" {
  218. return nil
  219. }
  220. state.SawText = true
  221. content := evt.Delta
  222. return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{Content: &content})}
  223. }
  224. func resToChatHandleOutputItemAdded(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  225. // function_call 与 custom_tool_call(custom/freeform 工具)均按工具调用注册,
  226. // 以便后续 *_input.delta / *_arguments.delta 能映射到正确的工具索引。
  227. if evt.Item == nil || (evt.Item.Type != "function_call" && evt.Item.Type != "custom_tool_call") {
  228. return nil
  229. }
  230. state.SawToolCall = true
  231. idx := state.NextToolCallIndex
  232. state.OutputIndexToToolIndex[evt.OutputIndex] = idx
  233. state.NextToolCallIndex++
  234. return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
  235. ToolCalls: []ChatToolCall{{
  236. Index: &idx,
  237. ID: evt.Item.CallID,
  238. Type: "function",
  239. Function: ChatFunctionCall{
  240. Name: evt.Item.Name,
  241. },
  242. }},
  243. })}
  244. }
  245. func resToChatHandleFuncArgsDelta(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  246. if evt.Delta == "" {
  247. return nil
  248. }
  249. idx, ok := state.OutputIndexToToolIndex[evt.OutputIndex]
  250. if !ok {
  251. return nil
  252. }
  253. state.OutputIndexToArguments[evt.OutputIndex] += evt.Delta
  254. return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
  255. ToolCalls: []ChatToolCall{{
  256. Index: &idx,
  257. Function: ChatFunctionCall{
  258. Arguments: evt.Delta,
  259. },
  260. }},
  261. })}
  262. }
  263. func resToChatHandleFuncArgsDone(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  264. idx, ok := state.OutputIndexToToolIndex[evt.OutputIndex]
  265. if !ok {
  266. return nil
  267. }
  268. completed := evt.Arguments
  269. if evt.Type == "response.custom_tool_call_input.done" {
  270. completed = evt.Input
  271. }
  272. current := state.OutputIndexToArguments[evt.OutputIndex]
  273. if completed == "" || !strings.HasPrefix(completed, current) || completed == current {
  274. return nil
  275. }
  276. remainder := completed[len(current):]
  277. state.OutputIndexToArguments[evt.OutputIndex] = completed
  278. return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
  279. ToolCalls: []ChatToolCall{{
  280. Index: &idx,
  281. Function: ChatFunctionCall{
  282. Arguments: remainder,
  283. },
  284. }},
  285. })}
  286. }
  287. func resToChatHandleReasoningDelta(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  288. if evt.Delta == "" {
  289. return nil
  290. }
  291. reasoning := evt.Delta
  292. return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{ReasoningContent: &reasoning})}
  293. }
  294. func resToChatHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
  295. state.Finalized = true
  296. finishReason := "stop"
  297. if evt.Usage != nil {
  298. state.Usage = chatUsageFromResponsesUsage(evt.Usage)
  299. }
  300. if evt.Response != nil {
  301. if evt.Response.Usage != nil {
  302. state.Usage = chatUsageFromResponsesUsage(evt.Response.Usage)
  303. }
  304. if evt.Response.ServiceTier != "" {
  305. state.ServiceTier = evt.Response.ServiceTier
  306. }
  307. switch evt.Response.Status {
  308. case "incomplete":
  309. if evt.Response.IncompleteDetails != nil {
  310. switch evt.Response.IncompleteDetails.Reason {
  311. case "max_output_tokens":
  312. finishReason = "length"
  313. case "content_filter":
  314. finishReason = "content_filter"
  315. }
  316. }
  317. case "completed":
  318. if state.SawToolCall {
  319. finishReason = "tool_calls"
  320. }
  321. }
  322. } else if state.SawToolCall {
  323. finishReason = "tool_calls"
  324. }
  325. var chunks []ChatCompletionsChunk
  326. chunks = append(chunks, makeChatFinishChunk(state, finishReason))
  327. if state.IncludeUsage && state.Usage != nil {
  328. chunks = append(chunks, ChatCompletionsChunk{
  329. ID: state.ID,
  330. Object: "chat.completion.chunk",
  331. Created: state.Created,
  332. Model: state.Model,
  333. ServiceTier: state.ServiceTier,
  334. Choices: []ChatChunkChoice{},
  335. Usage: state.Usage,
  336. })
  337. }
  338. return chunks
  339. }
  340. func chatUsageFromResponsesUsage(u *ResponsesUsage) *ChatUsage {
  341. if u == nil {
  342. return nil
  343. }
  344. usage := &ChatUsage{
  345. PromptTokens: u.InputTokens,
  346. CompletionTokens: u.OutputTokens,
  347. TotalTokens: u.InputTokens + u.OutputTokens,
  348. }
  349. usage.PromptTokensDetails = promptDetailsFromResponses(u.InputTokensDetails)
  350. if u.CacheCreationInputTokens > 0 {
  351. if usage.PromptTokensDetails == nil {
  352. usage.PromptTokensDetails = &ChatTokenDetails{}
  353. }
  354. if usage.PromptTokensDetails.CacheWriteTokens == 0 && usage.PromptTokensDetails.CacheCreationTokens == 0 {
  355. usage.PromptTokensDetails.CacheCreationTokens = u.CacheCreationInputTokens
  356. }
  357. }
  358. usage.CompletionTokensDetails = completionDetailsFromResponses(u.OutputTokensDetails)
  359. return usage
  360. }
  361. // promptDetailsFromResponses maps Responses-API input_tokens_details into a
  362. // Chat-Completions prompt_tokens_details. Returns nil when nothing would be
  363. // emitted, so upstreams that do not break down prompt usage stay clean.
  364. func promptDetailsFromResponses(src *ResponsesInputTokensDetails) *ChatTokenDetails {
  365. if src == nil {
  366. return nil
  367. }
  368. if src.CachedTokens == 0 && src.AudioTokens == 0 && src.CacheCreationTokens == 0 && src.CacheWriteTokens == 0 {
  369. return nil
  370. }
  371. return &ChatTokenDetails{
  372. CachedTokens: src.CachedTokens,
  373. AudioTokens: src.AudioTokens,
  374. CacheCreationTokens: src.CacheCreationTokens,
  375. CacheWriteTokens: src.CacheWriteTokens,
  376. }
  377. }
  378. // completionDetailsFromResponses maps Responses-API output_tokens_details
  379. // into a Chat-Completions completion_tokens_details. Mirrors the OpenAI
  380. // official CompletionUsage schema: reasoning_tokens, audio_tokens, and
  381. // the predicted-outputs accepted/rejected counts. Returns nil when nothing
  382. // would be emitted so non-reasoning, non-audio responses stay clean.
  383. func completionDetailsFromResponses(src *ResponsesOutputTokensDetails) *ChatTokenDetails {
  384. if src == nil {
  385. return nil
  386. }
  387. if src.ReasoningTokens == 0 && src.AudioTokens == 0 &&
  388. src.AcceptedPredictionTokens == 0 && src.RejectedPredictionTokens == 0 {
  389. return nil
  390. }
  391. return &ChatTokenDetails{
  392. ReasoningTokens: src.ReasoningTokens,
  393. AudioTokens: src.AudioTokens,
  394. AcceptedPredictionTokens: src.AcceptedPredictionTokens,
  395. RejectedPredictionTokens: src.RejectedPredictionTokens,
  396. }
  397. }
  398. func makeChatDeltaChunk(state *ResponsesEventToChatState, delta ChatDelta) ChatCompletionsChunk {
  399. return ChatCompletionsChunk{
  400. ID: state.ID,
  401. Object: "chat.completion.chunk",
  402. Created: state.Created,
  403. Model: state.Model,
  404. ServiceTier: state.ServiceTier,
  405. Choices: []ChatChunkChoice{{
  406. Index: 0,
  407. Delta: delta,
  408. FinishReason: nil,
  409. }},
  410. }
  411. }
  412. func makeChatFinishChunk(state *ResponsesEventToChatState, finishReason string) ChatCompletionsChunk {
  413. empty := ""
  414. return ChatCompletionsChunk{
  415. ID: state.ID,
  416. Object: "chat.completion.chunk",
  417. Created: state.Created,
  418. Model: state.Model,
  419. ServiceTier: state.ServiceTier,
  420. Choices: []ChatChunkChoice{{
  421. Index: 0,
  422. Delta: ChatDelta{Content: &empty},
  423. FinishReason: &finishReason,
  424. }},
  425. }
  426. }
  427. // generateChatCmplID returns a "chatcmpl-" prefixed random hex ID.
  428. func generateChatCmplID() string {
  429. b := make([]byte, 12)
  430. _, _ = rand.Read(b)
  431. return "chatcmpl-" + hex.EncodeToString(b)
  432. }
  433. // ---------------------------------------------------------------------------
  434. // BufferedResponseAccumulator: accumulates SSE delta events for non-streaming
  435. // paths where the terminal event may have empty output.
  436. // ---------------------------------------------------------------------------
  437. type bufferedFuncCall struct {
  438. OutputIndex int
  439. CallID string
  440. Name string
  441. Args strings.Builder
  442. }
  443. // BufferedResponseAccumulator collects content from Responses SSE delta events
  444. // so that non-streaming handlers can reconstruct output when the terminal event
  445. // (response.completed / response.done) carries an empty output array.
  446. type BufferedResponseAccumulator struct {
  447. text strings.Builder
  448. reasoning strings.Builder
  449. funcCalls []bufferedFuncCall
  450. outputIndexToFuncIdx map[int]int
  451. }
  452. // NewBufferedResponseAccumulator returns an initialised accumulator.
  453. func NewBufferedResponseAccumulator() *BufferedResponseAccumulator {
  454. return &BufferedResponseAccumulator{
  455. outputIndexToFuncIdx: make(map[int]int),
  456. }
  457. }
  458. // ProcessEvent inspects a single Responses SSE event and accumulates any
  459. // content it carries. Only delta events that contribute to the final output
  460. // are handled; all other event types are silently ignored.
  461. func (a *BufferedResponseAccumulator) ProcessEvent(event *ResponsesStreamEvent) {
  462. switch event.Type {
  463. case "response.output_text.delta":
  464. if event.Delta != "" {
  465. _, _ = a.text.WriteString(event.Delta)
  466. }
  467. case "response.output_item.added":
  468. if event.Item != nil && (event.Item.Type == "function_call" || event.Item.Type == "custom_tool_call") {
  469. idx := len(a.funcCalls)
  470. a.outputIndexToFuncIdx[event.OutputIndex] = idx
  471. a.funcCalls = append(a.funcCalls, bufferedFuncCall{
  472. OutputIndex: event.OutputIndex,
  473. CallID: event.Item.CallID,
  474. Name: event.Item.Name,
  475. })
  476. }
  477. case "response.function_call_arguments.delta", "response.custom_tool_call_input.delta":
  478. if event.Delta != "" {
  479. if idx, ok := a.outputIndexToFuncIdx[event.OutputIndex]; ok {
  480. _, _ = a.funcCalls[idx].Args.WriteString(event.Delta)
  481. }
  482. }
  483. case "response.function_call_arguments.done", "response.custom_tool_call_input.done":
  484. completed := event.Arguments
  485. if event.Type == "response.custom_tool_call_input.done" {
  486. completed = event.Input
  487. }
  488. if completed != "" {
  489. if idx, ok := a.outputIndexToFuncIdx[event.OutputIndex]; ok {
  490. a.funcCalls[idx].Args.Reset()
  491. _, _ = a.funcCalls[idx].Args.WriteString(completed)
  492. }
  493. }
  494. case "response.reasoning_summary_text.delta", "response.reasoning_text.delta":
  495. if event.Delta != "" {
  496. _, _ = a.reasoning.WriteString(event.Delta)
  497. }
  498. }
  499. }
  500. // HasContent reports whether any content has been accumulated.
  501. func (a *BufferedResponseAccumulator) HasContent() bool {
  502. return a.text.Len() > 0 || len(a.funcCalls) > 0 || a.reasoning.Len() > 0
  503. }
  504. // BuildOutput constructs a []ResponsesOutput from the accumulated delta
  505. // content. The order matches what ResponsesToChatCompletions expects:
  506. // reasoning → message → function_calls.
  507. func (a *BufferedResponseAccumulator) BuildOutput() []ResponsesOutput {
  508. var out []ResponsesOutput
  509. if a.reasoning.Len() > 0 {
  510. out = append(out, ResponsesOutput{
  511. Type: "reasoning",
  512. Summary: []ResponsesSummary{{
  513. Type: "summary_text",
  514. Text: a.reasoning.String(),
  515. }},
  516. })
  517. }
  518. if a.text.Len() > 0 {
  519. out = append(out, ResponsesOutput{
  520. Type: "message",
  521. Role: "assistant",
  522. Content: []ResponsesContentPart{{
  523. Type: "output_text",
  524. Text: a.text.String(),
  525. }},
  526. })
  527. }
  528. for i := range a.funcCalls {
  529. out = append(out, ResponsesOutput{
  530. Type: "function_call",
  531. CallID: a.funcCalls[i].CallID,
  532. Name: a.funcCalls[i].Name,
  533. Arguments: a.funcCalls[i].Args.String(),
  534. })
  535. }
  536. return out
  537. }
  538. // SupplementResponseOutput fills resp.Output from accumulated stream content
  539. // when the terminal event delivered an empty output array. It also fills empty
  540. // function-call arguments from authoritative argument-done events.
  541. func (a *BufferedResponseAccumulator) SupplementResponseOutput(resp *ResponsesResponse) {
  542. if resp == nil {
  543. return
  544. }
  545. if len(resp.Output) == 0 {
  546. if a.HasContent() {
  547. resp.Output = a.BuildOutput()
  548. }
  549. return
  550. }
  551. for outputIndex := range resp.Output {
  552. item := &resp.Output[outputIndex]
  553. if item.Type != "function_call" || item.Arguments != "" {
  554. continue
  555. }
  556. for funcIndex := range a.funcCalls {
  557. call := &a.funcCalls[funcIndex]
  558. matchesCallID := item.CallID != "" && item.CallID == call.CallID
  559. if !matchesCallID && call.OutputIndex != outputIndex {
  560. continue
  561. }
  562. if call.Args.Len() > 0 {
  563. item.Arguments = call.Args.String()
  564. }
  565. break
  566. }
  567. }
  568. }