diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go index 1567077cbb..88749207c9 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go @@ -61,6 +61,19 @@ func CustomToolNames(tools []ResponsesTool) map[string]bool { return out } +// HasToolSearchTool 判断 Responses 请求是否声明了 tool_search 服务端工具。chat 桥 +// 回程时需据此把模型对代理工具的调用还原为 tool_search_call 项:codex 只在该项类型 +// 且 execution=client 时执行 tool search,同名 function_call 会因 payload 不匹配 +// 触发 fatal 中止整个 turn。 +func HasToolSearchTool(tools []ResponsesTool) bool { + for _, tool := range tools { + if tool.Type == "tool_search" { + return true + } + } + return false +} + // responsesInputToChatMessages converts a Responses request's instructions + // input[] into Chat Completions messages. It is a three-stage pipeline: // @@ -661,8 +674,10 @@ func extractCustomToolCallInput(arguments string) string { // ChatCompletionsResponseToResponses converts a non-streaming Chat Completions // response into a Responses API response. customTools 是客户端请求中 custom 工具 -// 的名字集合(见 CustomToolNames),命中的调用会还原为 custom_tool_call 项。 -func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model string, customTools map[string]bool) *ResponsesResponse { +// 的名字集合(见 CustomToolNames),命中的调用会还原为 custom_tool_call 项; +// toolSearch 表示客户端声明了 tool_search 工具(见 HasToolSearchTool),代理工具 +// 的调用会还原为 tool_search_call 项。 +func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model string, customTools map[string]bool, toolSearch bool) *ResponsesResponse { id := "" if resp != nil { id = resp.ID @@ -687,7 +702,7 @@ func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model str if len(resp.Choices) > 0 { choice := resp.Choices[0] - out.Output = chatMessageToResponsesOutput(choice.Message, customTools) + out.Output = chatMessageToResponsesOutput(choice.Message, customTools, toolSearch) if choice.FinishReason == "length" { out.Status = "incomplete" out.IncompleteDetails = &ResponsesIncompleteDetails{Reason: "max_output_tokens"} @@ -702,7 +717,7 @@ func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model str return out } -func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bool) []ResponsesOutput { +func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bool, toolSearch bool) []ResponsesOutput { var outputs []ResponsesOutput if message.ReasoningContent != "" { outputs = append(outputs, ResponsesOutput{ @@ -748,6 +763,16 @@ func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bo }) continue } + if toolSearch && toolCall.Function.Name == toolSearchProxyName { + outputs = append(outputs, ResponsesOutput{ + Type: "tool_search_call", + ID: generateItemID(), + CallID: toolCall.ID, + Arguments: arguments, + Status: "completed", + }) + continue + } outputs = append(outputs, ResponsesOutput{ Type: "function_call", ID: generateItemID(), @@ -761,6 +786,21 @@ func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bo return outputs } +// toolSearchCallArgumentsJSON 把降级 function 调用累积的 arguments 字符串还原为 +// tool_search_call 线上要求的 JSON 对象;模型未按 schema 输出(非法 JSON)时按 +// 字符串值兜底,交由 codex 解析报错后让模型重试。 +func toolSearchCallArgumentsJSON(arguments string) json.RawMessage { + trimmed := strings.TrimSpace(arguments) + if trimmed == "" { + return json.RawMessage(`{}`) + } + if json.Valid([]byte(trimmed)) { + return json.RawMessage(trimmed) + } + fallback, _ := json.Marshal(arguments) + return fallback +} + func emptyResponsesMessageOutput() ResponsesOutput { return ResponsesOutput{ Type: "message", @@ -857,10 +897,18 @@ type ChatCompletionsToResponsesStreamState struct { // 路由回它注册的 custom 工具。 CustomTools map[string]bool + // ToolSearchDeclared 表示客户端请求声明了 tool_search 工具(见 + // HasToolSearchTool)。命中的代理调用按 tool_search_call 项还原,codex 只按 + // 该项类型(且 execution=client)执行 tool search。 + ToolSearchDeclared bool + // toolIsCustom 记录每个工具调用宣告时的类型判定,保证 added/done 事件的 // 项类型一致。 toolIsCustom map[int]bool + // toolIsToolSearch 记录工具调用是否判定为 tool_search 代理调用。 + toolIsToolSearch map[int]bool + // toolAnnounced 记录 output_item.added 是否已发出。存在 custom 工具且名字 // 尚未到达时延迟宣告,待名字可判定类型后再补发(见 announceChatToolItem)。 toolAnnounced map[int]bool @@ -872,14 +920,15 @@ type ChatCompletionsToResponsesStreamState struct { // NewChatCompletionsToResponsesStreamState returns an initialized stream state. func NewChatCompletionsToResponsesStreamState(model string) *ChatCompletionsToResponsesStreamState { return &ChatCompletionsToResponsesStreamState{ - ResponseID: generateResponsesID(), - Model: model, - Created: time.Now().Unix(), - ToolCalls: make(map[int]*ChatToolCall), - ToolItemIDs: make(map[int]string), - ToolOutputIndex: make(map[int]int), - toolIsCustom: make(map[int]bool), - toolAnnounced: make(map[int]bool), + ResponseID: generateResponsesID(), + Model: model, + Created: time.Now().Unix(), + ToolCalls: make(map[int]*ChatToolCall), + ToolItemIDs: make(map[int]string), + ToolOutputIndex: make(map[int]int), + toolIsCustom: make(map[int]bool), + toolIsToolSearch: make(map[int]bool), + toolAnnounced: make(map[int]bool), } } @@ -978,8 +1027,9 @@ func ChatCompletionsChunkToResponsesEvents( stored.Function.Arguments += toolCall.Function.Arguments // 未宣告(名字未到)时仅累积,宣告时统一补发;custom 调用的 // arguments 是包裹 input 的 JSON 片段,无法增量还原为自由文本 - // 输入,缓冲整份 arguments 收尾时一次性下发(见 closeChatToolItems)。 - if state.toolAnnounced[idx] && !state.toolIsCustom[idx] { + // 输入,缓冲整份 arguments 收尾时一次性下发(见 closeChatToolItems); + // tool_search 调用同样收尾时随 output_item.done 全量下发。 + if state.toolAnnounced[idx] && !state.toolIsCustom[idx] && !state.toolIsToolSearch[idx] { events = append(events, chatToResponsesEvent(state, "response.function_call_arguments.delta", &ResponsesStreamEvent{ OutputIndex: state.ToolOutputIndex[idx], ItemID: state.ToolItemIDs[idx], @@ -1210,16 +1260,21 @@ func announceChatToolItem( if state.toolAnnounced[idx] { return nil } - if !force && stored.Function.Name == "" && len(state.CustomTools) > 0 { + if !force && stored.Function.Name == "" && (len(state.CustomTools) > 0 || state.ToolSearchDeclared) { return nil } state.toolAnnounced[idx] = true isCustom := state.CustomTools[stored.Function.Name] + isToolSearch := !isCustom && state.ToolSearchDeclared && stored.Function.Name == toolSearchProxyName state.toolIsCustom[idx] = isCustom + state.toolIsToolSearch[idx] = isToolSearch itemType := "function_call" if isCustom { itemType = "custom_tool_call" } + if isToolSearch { + itemType = "tool_search_call" + } events := []ResponsesStreamEvent{chatToResponsesEvent(state, "response.output_item.added", &ResponsesStreamEvent{ OutputIndex: state.ToolOutputIndex[idx], Item: &ResponsesOutput{ @@ -1230,8 +1285,8 @@ func announceChatToolItem( Status: "in_progress", }, })} - // 迟到宣告时补发已累积的参数增量(custom 工具的输入收尾统一下发,不补发)。 - if !isCustom && stored.Function.Arguments != "" { + // 迟到宣告时补发已累积的参数增量(custom/tool_search 的输入收尾统一下发,不补发)。 + if !isCustom && !isToolSearch && stored.Function.Arguments != "" { events = append(events, chatToResponsesEvent(state, "response.function_call_arguments.delta", &ResponsesStreamEvent{ OutputIndex: state.ToolOutputIndex[idx], ItemID: state.ToolItemIDs[idx], @@ -1301,6 +1356,21 @@ func closeChatToolItems(state *ChatCompletionsToResponsesStreamState) []Response ) continue } + if state.toolIsToolSearch[i] { + // tool_search 调用按 tool_search_call 项收尾:codex 从 output_item.done + // 物化该调用(无参数增量事件),arguments 全量随项下发。 + events = append(events, chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{ + OutputIndex: outputIndex, + Item: &ResponsesOutput{ + Type: "tool_search_call", + ID: itemID, + CallID: toolCall.ID, + Arguments: arguments, + Status: "completed", + }, + })) + continue + } events = append(events, chatToResponsesEvent(state, "response.function_call_arguments.done", &ResponsesStreamEvent{ OutputIndex: outputIndex, @@ -1369,6 +1439,16 @@ func (state *ChatCompletionsToResponsesStreamState) chatOutput() []ResponsesOutp }) continue } + if state.toolIsToolSearch[i] { + outputs = append(outputs, ResponsesOutput{ + Type: "tool_search_call", + ID: generateItemID(), + CallID: toolCall.ID, + Arguments: arguments, + Status: "completed", + }) + continue + } outputs = append(outputs, ResponsesOutput{ Type: "function_call", ID: generateItemID(), diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_custom_tools_test.go b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_custom_tools_test.go index f7828a2d00..a7428860be 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_custom_tools_test.go +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_custom_tools_test.go @@ -103,7 +103,7 @@ func TestChatCompletionsResponseToResponses_CustomToolCallOutputItem(t *testing. }}, } - out := ChatCompletionsResponseToResponses(resp, "glm-5.2", map[string]bool{"exec": true}) + out := ChatCompletionsResponseToResponses(resp, "glm-5.2", map[string]bool{"exec": true}, false) require.Len(t, out.Output, 2) assert.Equal(t, "custom_tool_call", out.Output[0].Type) @@ -210,6 +210,131 @@ func TestResponsesToChatCompletionsRequest_ToolSearchToolBecomesProxyFunction(t assert.Contains(t, string(out.Tools[0].Function.Parameters), `"query"`) } +// codex 只在 ResponseItem 为 tool_search_call 变体且 execution=client 时执行 +// tool search;同名 function_call 会命中 ToolSearchHandler 后因 payload 不匹配 +// 触发 FunctionCallError::Fatal,直接中止整个 turn,因此回程必须还原项类型。 +func TestChatCompletionsResponseToResponses_ToolSearchCallOutputItem(t *testing.T) { + resp := &ChatCompletionsResponse{ + ID: "cc-1", + Choices: []ChatChoice{{ + Message: ChatMessage{ + Role: "assistant", + ToolCalls: []ChatToolCall{ + {ID: "call_s", Function: ChatFunctionCall{Name: "tool_search", Arguments: `{"query":"gmail","limit":2}`}}, + }, + }, + }}, + } + + out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, true) + require.Len(t, out.Output, 1) + + item := out.Output[0] + assert.Equal(t, "tool_search_call", item.Type) + assert.Equal(t, "call_s", item.CallID) + + // 线上形态:execution 必须为 "client"(codex 的必填字段,非 client 被忽略), + // arguments 必须是 JSON 对象而非字符串(codex 按对象解析 query/limit)。 + b, err := json.Marshal(item) + require.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal(b, &m)) + assert.Equal(t, "client", m["execution"]) + args, ok := m["arguments"].(map[string]any) + require.True(t, ok, "arguments 必须序列化为 JSON 对象") + assert.Equal(t, "gmail", args["query"]) +} + +func TestChatCompletionsResponseToResponses_ToolSearchNotDeclaredKeepsFunctionCall(t *testing.T) { + resp := &ChatCompletionsResponse{ + Choices: []ChatChoice{{ + Message: ChatMessage{ + Role: "assistant", + ToolCalls: []ChatToolCall{ + {ID: "call_s", Function: ChatFunctionCall{Name: "tool_search", Arguments: `{"query":"gmail"}`}}, + }, + }, + }}, + } + + // 客户端未声明 type=tool_search 时,同名普通 function 工具不受影响。 + out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, false) + require.Len(t, out.Output, 1) + assert.Equal(t, "function_call", out.Output[0].Type) +} + +func TestChatCompletionsChunkToResponsesEvents_ToolSearchCallStream(t *testing.T) { + state := NewChatCompletionsToResponsesStreamState("glm-5.2") + state.ToolSearchDeclared = true + + idx := 0 + chunk := &ChatCompletionsChunk{ + ID: "cc-1", + Choices: []ChatChunkChoice{{ + Delta: ChatDelta{ + ToolCalls: []ChatToolCall{{ + Index: &idx, + ID: "call_s", + Function: ChatFunctionCall{Name: "tool_search", Arguments: `{"query":"gmail"}`}, + }}, + }, + }}, + } + + events := ChatCompletionsChunkToResponsesEvents(chunk, state) + events = append(events, FinalizeChatCompletionsResponsesStream(state)...) + + var added, itemDone *ResponsesStreamEvent + for i := range events { + evt := &events[i] + switch evt.Type { + case "response.output_item.added": + if evt.Item != nil && evt.Item.Type != "message" && evt.Item.Type != "reasoning" { + added = evt + } + case "response.output_item.done": + if evt.Item != nil && evt.Item.Type == "tool_search_call" { + itemDone = evt + } + case "response.function_call_arguments.delta", "response.function_call_arguments.done", + "response.custom_tool_call_input.delta", "response.custom_tool_call_input.done": + t.Fatalf("tool_search 调用不应产出 %s", evt.Type) + } + } + + require.NotNil(t, added, "缺少 tool_search_call 的 output_item.added") + assert.Equal(t, "tool_search_call", added.Item.Type) + + require.NotNil(t, itemDone, "缺少 tool_search_call 的 output_item.done") + assert.Equal(t, "call_s", itemDone.Item.CallID) + + // SSE 线上形态经 responsesItemWire 白名单重组,必须单独断言。 + sse, err := ResponsesEventToSSE(*itemDone) + require.NoError(t, err) + assert.Contains(t, sse, `"execution":"client"`) + assert.Contains(t, sse, `"arguments":{"query":"gmail"}`) + assert.Contains(t, sse, `"call_id":"call_s"`) + + // response.completed 的 output 数组同样携带 tool_search_call 项。 + final := events[len(events)-1] + require.Equal(t, "response.completed", final.Type) + require.NotNil(t, final.Response) + found := false + for _, item := range final.Response.Output { + if item.Type == "tool_search_call" { + found = true + assert.Equal(t, "call_s", item.CallID) + } + } + assert.True(t, found, "response.completed 缺少 tool_search_call 输出项") +} + +func TestHasToolSearchTool(t *testing.T) { + assert.True(t, HasToolSearchTool([]ResponsesTool{{Type: "tool_search"}})) + assert.False(t, HasToolSearchTool([]ResponsesTool{{Type: "function", Name: "tool_search"}})) + assert.False(t, HasToolSearchTool(nil)) +} + func TestResponsesToChatCompletionsRequest_NamespaceToolFlattensChildren(t *testing.T) { req := &ResponsesRequest{ Model: "glm-5.2", diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_test.go b/backend/internal/pkg/apicompat/chatcompletions_responses_test.go index a703f0052b..fdb008b261 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_responses_test.go +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_test.go @@ -362,7 +362,7 @@ func TestChatCompletionsResponseToResponses_DeepSeekReasoningOnlyFallsBackToMess }}, } - out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil) + out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false) require.Len(t, out.Output, 2) require.Equal(t, "reasoning", out.Output[0].Type) @@ -396,7 +396,7 @@ func TestChatCompletionsResponseToResponses_DeepSeekReasoningToolCallDoesNotFall }}, } - out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil) + out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false) require.Len(t, out.Output, 2) require.Equal(t, "reasoning", out.Output[0].Type) diff --git a/backend/internal/pkg/apicompat/responses_stream_event_wire.go b/backend/internal/pkg/apicompat/responses_stream_event_wire.go index 6eaead4459..58b6831b10 100644 --- a/backend/internal/pkg/apicompat/responses_stream_event_wire.go +++ b/backend/internal/pkg/apicompat/responses_stream_event_wire.go @@ -86,6 +86,23 @@ func (e ResponsesStreamEvent) MarshalJSON() ([]byte, error) { } return json.Marshal(m) + case "response.custom_tool_call_input.delta", "response.custom_tool_call_input.done": + m := e.wireBase() + e.putItemID(m) + m["output_index"] = e.OutputIndex + if e.CallID != "" { + m["call_id"] = e.CallID + } + if e.Name != "" { + m["name"] = e.Name + } + if e.Type == "response.custom_tool_call_input.done" { + m["input"] = e.Input + } else { + m["delta"] = e.Delta + } + return json.Marshal(m) + default: // response.created / completed / done / failed / incomplete and any // event type not shaped above keep the default struct marshalling. @@ -173,6 +190,12 @@ func responsesItemWire(item *ResponsesOutput) map[string]any { m["call_id"] = item.CallID m["name"] = item.Name m["input"] = item.Input + case "tool_search_call": + // tool_search 调用还原项:execution 必须为 "client"(否则 codex 忽略该 + // 调用),arguments 在线上是 JSON 对象而非字符串。 + m["call_id"] = item.CallID + m["execution"] = "client" + m["arguments"] = toolSearchCallArgumentsJSON(item.Arguments) } return m } diff --git a/backend/internal/pkg/apicompat/responses_stream_event_wire_test.go b/backend/internal/pkg/apicompat/responses_stream_event_wire_test.go index b4f6871d5f..f44f3e7770 100644 --- a/backend/internal/pkg/apicompat/responses_stream_event_wire_test.go +++ b/backend/internal/pkg/apicompat/responses_stream_event_wire_test.go @@ -102,6 +102,26 @@ func TestWire_ArgumentsDonePresentEvenEmpty(t *testing.T) { require.Equal(t, "", m["arguments"]) } +// TestWire_CustomToolCallInputIndexPresentAtZero guards the omitempty trap for +// custom_tool_call_input.delta/done: output_index must serialize even when 0 +// (custom tool call as the first output item). +func TestWire_CustomToolCallInputIndexPresentAtZero(t *testing.T) { + d := marshalEvent(t, ResponsesStreamEvent{ + Type: "response.custom_tool_call_input.delta", OutputIndex: 0, ItemID: "ct_1", Delta: "dir", + }) + require.Contains(t, d, "output_index") + require.EqualValues(t, 0, d["output_index"]) + require.Equal(t, "dir", d["delta"]) + + done := marshalEvent(t, ResponsesStreamEvent{ + Type: "response.custom_tool_call_input.done", OutputIndex: 0, ItemID: "ct_1", CallID: "call_1", Name: "exec", Input: "dir", + }) + require.Contains(t, done, "output_index") + require.EqualValues(t, 0, done["output_index"]) + require.Equal(t, "dir", done["input"]) + require.NotContains(t, done, "delta") +} + // TestWire_UnknownEventFallsBackToDefault ensures non-streamed event types keep // default marshalling (the response object is preserved). func TestWire_UnknownEventFallsBackToDefault(t *testing.T) { diff --git a/backend/internal/pkg/apicompat/types.go b/backend/internal/pkg/apicompat/types.go index ebfe1b6782..25baa7a080 100644 --- a/backend/internal/pkg/apicompat/types.go +++ b/backend/internal/pkg/apicompat/types.go @@ -328,6 +328,28 @@ type ResponsesOutput struct { Action *WebSearchAction `json:"action,omitempty"` } +// MarshalJSON 处理 tool_search_call 项的线上形态(复用 CallID/Arguments 字段): +// execution 固定为 "client"(codex 的必填字段,非 client 的调用会被静默忽略), +// arguments 是 JSON 对象而非 function_call 语义下的字符串。其余类型走默认结构体 +// 序列化,输出逐字节不变。 +func (o ResponsesOutput) MarshalJSON() ([]byte, error) { + type responsesOutputAlias ResponsesOutput + if o.Type != "tool_search_call" { + return json.Marshal(responsesOutputAlias(o)) + } + m := map[string]any{ + "type": o.Type, + "id": o.ID, + "call_id": o.CallID, + "execution": "client", + "arguments": toolSearchCallArgumentsJSON(o.Arguments), + } + if o.Status != "" { + m["status"] = o.Status + } + return json.Marshal(m) +} + // WebSearchAction describes the search action in a web_search_call output item. type WebSearchAction struct { Type string `json:"type,omitempty"` // "search" diff --git a/backend/internal/service/openai_gateway_responses_chat_fallback.go b/backend/internal/service/openai_gateway_responses_chat_fallback.go index a609e8699f..1c75c746d6 100644 --- a/backend/internal/service/openai_gateway_responses_chat_fallback.go +++ b/backend/internal/service/openai_gateway_responses_chat_fallback.go @@ -54,8 +54,10 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions( reasoningEffort := extractOpenAIReasoningEffortFromBody(body, originalModel) serviceTier := extractOpenAIServiceTierFromBody(body) // custom 工具(如 codex 的 exec)降级为 function 工具转发,回程需按名字还原为 - // custom_tool_call 项,先记下名字集合。 + // custom_tool_call 项,先记下名字集合;tool_search 工具同理,回程还原为 + // tool_search_call 项。 customTools := apicompat.CustomToolNames(responsesReq.Tools) + toolSearch := apicompat.HasToolSearchTool(responsesReq.Tools) chatReq, err := apicompat.ResponsesToChatCompletionsRequest(&responsesReq) if err != nil { @@ -194,9 +196,9 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions( } if clientStream { - return s.streamChatCompletionsAsResponses(c, resp, originalModel, customTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime) + return s.streamChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime) } - return s.bufferChatCompletionsAsResponses(c, resp, originalModel, customTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime) + return s.bufferChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime) } func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses( @@ -204,6 +206,7 @@ func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses( resp *http.Response, originalModel string, customTools map[string]bool, + toolSearch bool, billingModel string, upstreamModel string, reasoningEffort *string, @@ -234,7 +237,7 @@ func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses( }) return nil, fmt.Errorf("parse chat completions response: %w", err) } - responsesResp := apicompat.ChatCompletionsResponseToResponses(&ccResp, originalModel, customTools) + responsesResp := apicompat.ChatCompletionsResponseToResponses(&ccResp, originalModel, customTools, toolSearch) usage := OpenAIUsage{} if parsed, ok := extractOpenAIUsageFromJSONBytes(respBody); ok { @@ -264,6 +267,7 @@ func (s *OpenAIGatewayService) streamChatCompletionsAsResponses( resp *http.Response, originalModel string, customTools map[string]bool, + toolSearch bool, billingModel string, upstreamModel string, reasoningEffort *string, @@ -289,6 +293,7 @@ func (s *OpenAIGatewayService) streamChatCompletionsAsResponses( state := apicompat.NewChatCompletionsToResponsesStreamState(originalModel) state.CustomTools = customTools + state.ToolSearchDeclared = toolSearch var usage OpenAIUsage var firstTokenMs *int clientDisconnected := false