mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(apicompat): 添加 tool_search 支持,增强工具调用处理逻辑
This commit is contained in:
@@ -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(),
|
||||
|
||||
+126
-1
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user