mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-06 16:03:49 +08:00
Merge pull request #5905 from wucm667/fix/issue-5883-restore-custom-tool-alias
fix(openai): restore namespaced custom tool aliases
This commit is contained in:
@@ -674,7 +674,7 @@ func TestDirectBridge_NonStreamingMatchesDoubleConversion(t *testing.T) {
|
||||
direct := ChatCompletionsResponseToAnthropic(resp, "claude-sonnet-4-20250514")
|
||||
|
||||
// Double-conversion bridge
|
||||
responsesResp := ChatCompletionsResponseToResponses(resp, "claude-sonnet-4-20250514", nil, false, nil)
|
||||
responsesResp := ChatCompletionsResponseToResponses(resp, "claude-sonnet-4-20250514", nil, nil, false, nil)
|
||||
double := ResponsesToAnthropic(responsesResp, "claude-sonnet-4-20250514")
|
||||
|
||||
// Compare key fields
|
||||
@@ -988,7 +988,7 @@ func TestDirectBridge_NonStreamingMatchesDoubleConversion_CacheWriteTokens(t *te
|
||||
|
||||
direct := ChatCompletionsResponseToAnthropic(resp, "claude-sonnet-4-20250514")
|
||||
|
||||
responsesResp := ChatCompletionsResponseToResponses(resp, "claude-sonnet-4-20250514", nil, false, nil)
|
||||
responsesResp := ChatCompletionsResponseToResponses(resp, "claude-sonnet-4-20250514", nil, nil, false, nil)
|
||||
double := ResponsesToAnthropic(responsesResp, "claude-sonnet-4-20250514")
|
||||
|
||||
require.Equal(t, double.Usage.InputTokens, direct.Usage.InputTokens)
|
||||
@@ -1058,7 +1058,7 @@ func TestDirectBridge_NonStreamingMatchesDoubleConversion_EmptyChoices(t *testin
|
||||
|
||||
direct := ChatCompletionsResponseToAnthropic(resp, "claude-sonnet-4-20250514")
|
||||
|
||||
responsesResp := ChatCompletionsResponseToResponses(resp, "claude-sonnet-4-20250514", nil, false, nil)
|
||||
responsesResp := ChatCompletionsResponseToResponses(resp, "claude-sonnet-4-20250514", nil, nil, false, nil)
|
||||
double := ResponsesToAnthropic(responsesResp, "claude-sonnet-4-20250514")
|
||||
|
||||
require.Equal(t, AnthropicStopReasonString(double.StopReason), AnthropicStopReasonString(direct.StopReason))
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestChatReasoningAlias_AnthropicStreaming(t *testing.T) {
|
||||
func TestChatReasoningAlias_ResponsesSharedPaths(t *testing.T) {
|
||||
var response ChatCompletionsResponse
|
||||
readIssue5302Fixture(t, "nonstream_reasoning.json", &response)
|
||||
nonStream := ChatCompletionsResponseToResponses(&response, "reasoning-model", nil, false, nil)
|
||||
nonStream := ChatCompletionsResponseToResponses(&response, "reasoning-model", nil, nil, false, nil)
|
||||
require.Len(t, nonStream.Output, 2)
|
||||
require.Equal(t, "reasoning", nonStream.Output[0].Type)
|
||||
require.Equal(t, "fallback reasoning", nonStream.Output[0].Summary[0].Text)
|
||||
|
||||
@@ -159,6 +159,20 @@ func CustomToolNames(tools []ResponsesTool) map[string]bool {
|
||||
return out
|
||||
}
|
||||
|
||||
// FunctionToolNames collects explicitly declared top-level function tools.
|
||||
func FunctionToolNames(tools []ResponsesTool) map[string]bool {
|
||||
var out map[string]bool
|
||||
for _, tool := range tools {
|
||||
if tool.Type == "function" && tool.Name != "" {
|
||||
if out == nil {
|
||||
out = make(map[string]bool)
|
||||
}
|
||||
out[tool.Name] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// NamespacedToolName 记录 namespace 子工具的原始归属(命名空间 + 裸子工具名)。
|
||||
type NamespacedToolName struct {
|
||||
Namespace string
|
||||
@@ -197,6 +211,43 @@ func NamespaceToolNames(tools []ResponsesTool) map[string]NamespacedToolName {
|
||||
return out
|
||||
}
|
||||
|
||||
// customToolCallName restores both the exact downgraded custom-tool name and
|
||||
// namespace-prefixed aliases that chat models sometimes infer from neighboring
|
||||
// flattened namespace tools (for example functions__exec beside
|
||||
// functions__wait). A real declared namespace child always owns its flattened
|
||||
// name, and ambiguous aliases are left as ordinary function calls.
|
||||
func customToolCallName(name string, customTools, functionTools map[string]bool, namespaceTools map[string]NamespacedToolName) (string, bool) {
|
||||
if functionTools[name] {
|
||||
return "", false
|
||||
}
|
||||
if customTools[name] {
|
||||
return name, true
|
||||
}
|
||||
if _, ok := namespaceTools[name]; ok {
|
||||
return "", false
|
||||
}
|
||||
match := ""
|
||||
for customName := range customTools {
|
||||
for _, namespaceTool := range namespaceTools {
|
||||
if flattenNamespaceToolName(namespaceTool.Namespace, customName) != name {
|
||||
continue
|
||||
}
|
||||
if match != "" && match != customName {
|
||||
return "", false
|
||||
}
|
||||
match = customName
|
||||
}
|
||||
}
|
||||
return match, match != ""
|
||||
}
|
||||
|
||||
func customNameForStreamTool(state *ChatCompletionsToResponsesStreamState, name string) string {
|
||||
if customName, ok := customToolCallName(name, state.CustomTools, state.FunctionTools, state.NamespaceTools); ok {
|
||||
return customName
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// HasToolSearchTool 判断 Responses 请求是否声明了 tool_search 服务端工具。chat 桥
|
||||
// 回程时需据此把模型对代理工具的调用还原为 tool_search_call 项:codex 只在该项类型
|
||||
// 且 execution=client 时执行 tool search,同名 function_call 会因 payload 不匹配
|
||||
@@ -918,6 +969,9 @@ func responsesToolsToChatTools(tools []ResponsesTool) ([]ChatTool, error) {
|
||||
topLevel := make(map[string]bool)
|
||||
for _, tool := range tools {
|
||||
if (tool.Type == "function" || tool.Type == "custom") && tool.Name != "" {
|
||||
if topLevel[tool.Name] {
|
||||
return nil, fmt.Errorf("duplicate top-level executable tool name %q; this upstream cannot disambiguate duplicate names, rename one of the tools", tool.Name)
|
||||
}
|
||||
topLevel[tool.Name] = true
|
||||
}
|
||||
}
|
||||
@@ -1148,7 +1202,7 @@ func extractCustomToolCallInput(arguments string) string {
|
||||
// toolSearch 表示客户端声明了 tool_search 工具(见 HasToolSearchTool),代理工具
|
||||
// 的调用会还原为 tool_search_call 项;namespaceTools 是 namespace 子工具的摊平名
|
||||
// 映射(见 NamespaceToolNames),命中的调用还原为带 namespace 字段的 function_call 项。
|
||||
func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model string, customTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) *ResponsesResponse {
|
||||
func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model string, customTools, functionTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) *ResponsesResponse {
|
||||
id := ""
|
||||
if resp != nil {
|
||||
id = resp.ID
|
||||
@@ -1173,7 +1227,7 @@ func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model str
|
||||
|
||||
if len(resp.Choices) > 0 {
|
||||
choice := resp.Choices[0]
|
||||
out.Output = chatMessageToResponsesOutput(choice.Message, customTools, toolSearch, namespaceTools)
|
||||
out.Output = chatMessageToResponsesOutput(choice.Message, customTools, functionTools, toolSearch, namespaceTools)
|
||||
if choice.FinishReason == "length" {
|
||||
out.Status = "incomplete"
|
||||
out.IncompleteDetails = &ResponsesIncompleteDetails{Reason: "max_output_tokens"}
|
||||
@@ -1188,7 +1242,7 @@ func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model str
|
||||
return out
|
||||
}
|
||||
|
||||
func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) []ResponsesOutput {
|
||||
func chatMessageToResponsesOutput(message ChatMessage, customTools, functionTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) []ResponsesOutput {
|
||||
var outputs []ResponsesOutput
|
||||
reasoning := message.reasoningText()
|
||||
if reasoning != "" {
|
||||
@@ -1224,12 +1278,12 @@ func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bo
|
||||
if strings.TrimSpace(arguments) == "" {
|
||||
arguments = "{}"
|
||||
}
|
||||
if customTools[toolCall.Function.Name] {
|
||||
if customName, ok := customToolCallName(toolCall.Function.Name, customTools, functionTools, namespaceTools); ok {
|
||||
outputs = append(outputs, ResponsesOutput{
|
||||
Type: "custom_tool_call",
|
||||
ID: generateItemID(),
|
||||
CallID: toolCall.ID,
|
||||
Name: toolCall.Function.Name,
|
||||
Name: customName,
|
||||
Input: extractCustomToolCallInput(arguments),
|
||||
Status: "completed",
|
||||
})
|
||||
@@ -1396,6 +1450,9 @@ type ChatCompletionsToResponsesStreamState struct {
|
||||
// 路由回它注册的 custom 工具。
|
||||
CustomTools map[string]bool
|
||||
|
||||
// FunctionTools is the set of explicitly declared top-level function tools.
|
||||
FunctionTools map[string]bool
|
||||
|
||||
// ToolSearchDeclared 表示客户端请求声明了 tool_search 工具(见
|
||||
// HasToolSearchTool)。命中的代理调用按 tool_search_call 项还原,codex 只按
|
||||
// 该项类型(且 execution=client)执行 tool search。
|
||||
@@ -1796,11 +1853,11 @@ func announceChatToolItem(
|
||||
if state.toolAnnounced[idx] {
|
||||
return nil
|
||||
}
|
||||
if !force && stored.Function.Name == "" && (len(state.CustomTools) > 0 || state.ToolSearchDeclared || len(state.NamespaceTools) > 0) {
|
||||
if !force && stored.Function.Name == "" && (len(state.CustomTools) > 0 || len(state.FunctionTools) > 0 || state.ToolSearchDeclared || len(state.NamespaceTools) > 0) {
|
||||
return nil
|
||||
}
|
||||
state.toolAnnounced[idx] = true
|
||||
isCustom := state.CustomTools[stored.Function.Name]
|
||||
customName, isCustom := customToolCallName(stored.Function.Name, state.CustomTools, state.FunctionTools, state.NamespaceTools)
|
||||
isToolSearch := !isCustom && state.ToolSearchDeclared && stored.Function.Name == toolSearchProxyName
|
||||
state.toolIsCustom[idx] = isCustom
|
||||
state.toolIsToolSearch[idx] = isToolSearch
|
||||
@@ -1814,6 +1871,9 @@ func announceChatToolItem(
|
||||
// namespace 子工具的调用仍按 function_call 生命周期下发,但 added/done 项要
|
||||
// 还原为裸子工具名 + namespace 字段(codex 按 namespace+name 路由)。
|
||||
itemName, itemNamespace := stored.Function.Name, ""
|
||||
if isCustom {
|
||||
itemName = customName
|
||||
}
|
||||
if ns, ok := state.NamespaceTools[stored.Function.Name]; ok && !isCustom && !isToolSearch {
|
||||
state.toolNamespace[idx] = ns
|
||||
itemName, itemNamespace = ns.Name, ns.Namespace
|
||||
@@ -1883,7 +1943,7 @@ func closeChatToolItems(state *ChatCompletionsToResponsesStreamState) []Response
|
||||
OutputIndex: outputIndex,
|
||||
ItemID: itemID,
|
||||
CallID: toolCall.ID,
|
||||
Name: toolCall.Function.Name,
|
||||
Name: customNameForStreamTool(state, toolCall.Function.Name),
|
||||
Input: input,
|
||||
}),
|
||||
chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{
|
||||
@@ -1892,7 +1952,7 @@ func closeChatToolItems(state *ChatCompletionsToResponsesStreamState) []Response
|
||||
Type: "custom_tool_call",
|
||||
ID: itemID,
|
||||
CallID: toolCall.ID,
|
||||
Name: toolCall.Function.Name,
|
||||
Name: customNameForStreamTool(state, toolCall.Function.Name),
|
||||
Input: input,
|
||||
Status: "completed",
|
||||
},
|
||||
@@ -1983,7 +2043,7 @@ func (state *ChatCompletionsToResponsesStreamState) chatOutput() []ResponsesOutp
|
||||
Type: "custom_tool_call",
|
||||
ID: generateItemID(),
|
||||
CallID: toolCall.ID,
|
||||
Name: toolCall.Function.Name,
|
||||
Name: customNameForStreamTool(state, toolCall.Function.Name),
|
||||
Input: extractCustomToolCallInput(arguments),
|
||||
Status: "completed",
|
||||
})
|
||||
|
||||
+137
-4
@@ -34,6 +34,60 @@ func TestResponsesToChatCompletionsRequest_CustomToolBecomesFunctionTool(t *test
|
||||
assert.Equal(t, "wait", out.Tools[1].Function.Name)
|
||||
}
|
||||
|
||||
func TestResponsesChatBridge_MixedCustomAndNamespaceToolNames(t *testing.T) {
|
||||
req := &ResponsesRequest{
|
||||
Model: "deepseek-test",
|
||||
Input: json.RawMessage(`"run pwd"`),
|
||||
Tools: []ResponsesTool{
|
||||
{Type: "custom", Name: "exec", Description: "Runs a command"},
|
||||
{Type: "namespace", Name: "functions", Tools: []ResponsesTool{
|
||||
{Type: "function", Name: "wait", Parameters: json.RawMessage(`{"type":"object"}`)},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
chatReq, err := ResponsesToChatCompletionsRequest(req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, chatReq.Tools, 2)
|
||||
assert.Equal(t, "exec", chatReq.Tools[0].Function.Name)
|
||||
assert.Equal(t, "functions__wait", chatReq.Tools[1].Function.Name)
|
||||
|
||||
customTools := CustomToolNames(req.Tools)
|
||||
namespaceTools := NamespaceToolNames(req.Tools)
|
||||
resp := &ChatCompletionsResponse{Choices: []ChatChoice{{Message: ChatMessage{ToolCalls: []ChatToolCall{
|
||||
{ID: "call_exact", Function: ChatFunctionCall{Name: "exec", Arguments: `{"input":"pwd"}`}},
|
||||
{ID: "call_wait", Function: ChatFunctionCall{Name: "functions__wait", Arguments: `{"cell_id":"1"}`}},
|
||||
{ID: "call_alias", Function: ChatFunctionCall{Name: "functions__exec", Arguments: `not-json`}},
|
||||
}}}}}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, req.Model, customTools, FunctionToolNames(req.Tools), false, namespaceTools)
|
||||
require.Len(t, out.Output, 3)
|
||||
assert.Equal(t, "custom_tool_call", out.Output[0].Type)
|
||||
assert.Equal(t, "exec", out.Output[0].Name)
|
||||
assert.Equal(t, "pwd", out.Output[0].Input)
|
||||
assert.Equal(t, "function_call", out.Output[1].Type)
|
||||
assert.Equal(t, "functions", out.Output[1].Namespace)
|
||||
assert.Equal(t, "wait", out.Output[1].Name)
|
||||
assert.Equal(t, "custom_tool_call", out.Output[2].Type)
|
||||
assert.Equal(t, "exec", out.Output[2].Name)
|
||||
assert.Equal(t, "not-json", out.Output[2].Input)
|
||||
}
|
||||
|
||||
func TestChatCompletionsResponseToResponses_ExplicitFunctionOwnsCustomAliasCollision(t *testing.T) {
|
||||
resp := &ChatCompletionsResponse{Choices: []ChatChoice{{Message: ChatMessage{ToolCalls: []ChatToolCall{{
|
||||
ID: "call_function", Function: ChatFunctionCall{Name: "functions__exec", Arguments: `{"path":"/tmp"}`},
|
||||
}}}}}}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-test",
|
||||
map[string]bool{"exec": true}, map[string]bool{"functions__exec": true}, false,
|
||||
map[string]NamespacedToolName{"functions__wait": {Namespace: "functions", Name: "wait"}})
|
||||
|
||||
require.Len(t, out.Output, 1)
|
||||
assert.Equal(t, "function_call", out.Output[0].Type)
|
||||
assert.Equal(t, "functions__exec", out.Output[0].Name)
|
||||
assert.Equal(t, `{"path":"/tmp"}`, out.Output[0].Arguments)
|
||||
}
|
||||
|
||||
func TestResponsesToChatCompletionsRequest_AdditionalToolsItem(t *testing.T) {
|
||||
req := &ResponsesRequest{
|
||||
Model: "gpt-test",
|
||||
@@ -173,7 +227,7 @@ func TestChatCompletionsResponseToResponses_CustomToolCallOutputItem(t *testing.
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", map[string]bool{"exec": true}, false, nil)
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", map[string]bool{"exec": true}, nil, false, nil)
|
||||
require.Len(t, out.Output, 2)
|
||||
|
||||
assert.Equal(t, "custom_tool_call", out.Output[0].Type)
|
||||
@@ -264,6 +318,68 @@ func TestChatCompletionsChunkToResponsesEvents_CustomToolCallStream(t *testing.T
|
||||
assert.True(t, foundCustom, "response.completed 缺少 custom_tool_call 输出项")
|
||||
}
|
||||
|
||||
func TestChatCompletionsChunkToResponsesEvents_MixedCustomNamespaceAliasStream(t *testing.T) {
|
||||
state := NewChatCompletionsToResponsesStreamState("deepseek-test")
|
||||
state.CustomTools = map[string]bool{"exec": true}
|
||||
state.NamespaceTools = map[string]NamespacedToolName{
|
||||
"functions__wait": {Namespace: "functions", Name: "wait"},
|
||||
}
|
||||
|
||||
idx := 0
|
||||
chunk := &ChatCompletionsChunk{Choices: []ChatChunkChoice{{Delta: ChatDelta{ToolCalls: []ChatToolCall{{
|
||||
Index: &idx, ID: "call_alias", Function: ChatFunctionCall{Name: "functions__exec", Arguments: `not-json`},
|
||||
}}}}}}
|
||||
events := ChatCompletionsChunkToResponsesEvents(chunk, state)
|
||||
events = append(events, FinalizeChatCompletionsResponsesStream(state)...)
|
||||
|
||||
for _, evt := range events {
|
||||
if evt.Type == "response.output_item.added" && evt.Item != nil && evt.Item.Type == "custom_tool_call" {
|
||||
assert.Equal(t, "exec", evt.Item.Name)
|
||||
}
|
||||
if evt.Type == "response.custom_tool_call_input.done" {
|
||||
assert.Equal(t, "exec", evt.Name)
|
||||
assert.Equal(t, "not-json", evt.Input)
|
||||
}
|
||||
if evt.Type == "response.output_item.done" && evt.Item != nil && evt.Item.Type == "custom_tool_call" {
|
||||
assert.Equal(t, "exec", evt.Item.Name)
|
||||
assert.Equal(t, "not-json", evt.Item.Input)
|
||||
}
|
||||
}
|
||||
|
||||
final := events[len(events)-1]
|
||||
require.Equal(t, "response.completed", final.Type)
|
||||
require.Len(t, final.Response.Output, 1)
|
||||
assert.Equal(t, "custom_tool_call", final.Response.Output[0].Type)
|
||||
assert.Equal(t, "exec", final.Response.Output[0].Name)
|
||||
assert.Equal(t, "not-json", final.Response.Output[0].Input)
|
||||
}
|
||||
|
||||
func TestChatCompletionsChunkToResponsesEvents_ExplicitFunctionOwnsCustomAliasCollision(t *testing.T) {
|
||||
state := NewChatCompletionsToResponsesStreamState("deepseek-test")
|
||||
state.CustomTools = map[string]bool{"exec": true}
|
||||
state.FunctionTools = map[string]bool{"functions__exec": true}
|
||||
state.NamespaceTools = map[string]NamespacedToolName{
|
||||
"functions__wait": {Namespace: "functions", Name: "wait"},
|
||||
}
|
||||
|
||||
idx := 0
|
||||
chunk := &ChatCompletionsChunk{Choices: []ChatChunkChoice{{Delta: ChatDelta{ToolCalls: []ChatToolCall{{
|
||||
Index: &idx, ID: "call_function", Function: ChatFunctionCall{Name: "functions__exec", Arguments: `{"path":"/tmp"}`},
|
||||
}}}}}}
|
||||
events := ChatCompletionsChunkToResponsesEvents(chunk, state)
|
||||
events = append(events, FinalizeChatCompletionsResponsesStream(state)...)
|
||||
|
||||
for _, evt := range events {
|
||||
assert.NotEqual(t, "response.custom_tool_call_input.done", evt.Type)
|
||||
}
|
||||
final := events[len(events)-1]
|
||||
require.Equal(t, "response.completed", final.Type)
|
||||
require.Len(t, final.Response.Output, 1)
|
||||
assert.Equal(t, "function_call", final.Response.Output[0].Type)
|
||||
assert.Equal(t, "functions__exec", final.Response.Output[0].Name)
|
||||
assert.Equal(t, `{"path":"/tmp"}`, final.Response.Output[0].Arguments)
|
||||
}
|
||||
|
||||
func TestResponsesToChatCompletionsRequest_ToolSearchToolBecomesProxyFunction(t *testing.T) {
|
||||
req := &ResponsesRequest{
|
||||
Model: "glm-5.2",
|
||||
@@ -308,7 +424,7 @@ func TestChatCompletionsResponseToResponses_ToolSearchCallOutputItem(t *testing.
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, true, nil)
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, nil, true, nil)
|
||||
require.Len(t, out.Output, 1)
|
||||
|
||||
item := out.Output[0]
|
||||
@@ -340,7 +456,7 @@ func TestChatCompletionsResponseToResponses_ToolSearchNotDeclaredKeepsFunctionCa
|
||||
}
|
||||
|
||||
// 客户端未声明 type=tool_search 时,同名普通 function 工具不受影响。
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, false, nil)
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, nil, false, nil)
|
||||
require.Len(t, out.Output, 1)
|
||||
assert.Equal(t, "function_call", out.Output[0].Type)
|
||||
}
|
||||
@@ -656,6 +772,23 @@ func TestResponsesToChatCompletionsRequest_RejectsToolSearchNameConflict(t *test
|
||||
assert.Equal(t, "tool_search", out.Tools[0].Function.Name)
|
||||
}
|
||||
|
||||
func TestResponsesToChatCompletionsRequest_RejectsDuplicateTopLevelExecutableNames(t *testing.T) {
|
||||
for _, tools := range [][]ResponsesTool{
|
||||
{{Type: "custom", Name: "exec"}, {Type: "function", Name: "exec"}},
|
||||
{{Type: "function", Name: "exec"}, {Type: "function", Name: "exec"}},
|
||||
{{Type: "custom", Name: "exec"}, {Type: "custom", Name: "exec"}},
|
||||
} {
|
||||
_, err := ResponsesToChatCompletionsRequest(&ResponsesRequest{
|
||||
Model: "glm-5.2",
|
||||
Input: json.RawMessage(`"hi"`),
|
||||
Tools: tools,
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exec")
|
||||
assert.Contains(t, err.Error(), "cannot disambiguate")
|
||||
}
|
||||
}
|
||||
|
||||
// tool_choice 指向被转换丢弃的工具(如 web_search)或不存在的名字时不能原样转发,
|
||||
// chat 上游会因选择项指向未声明工具而 400;字符串形式与指向幸存工具的选择保持转发。
|
||||
func TestResponsesToChatCompletionsRequest_DropsToolChoiceForDroppedTool(t *testing.T) {
|
||||
@@ -807,7 +940,7 @@ func TestChatCompletionsResponseToResponses_NamespacedToolCallRestored(t *testin
|
||||
"mcp__svc__echo": {Namespace: "mcp__svc", Name: "echo"},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, false, nsTools)
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, nil, false, nsTools)
|
||||
require.Len(t, out.Output, 2)
|
||||
|
||||
item := out.Output[0]
|
||||
|
||||
@@ -65,7 +65,7 @@ func TestChatCompletionsResponseToResponses_SkipsInvalidFunctionArguments(t *tes
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-v4-flash", nil, false, nil)
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-v4-flash", nil, nil, false, nil)
|
||||
require.Equal(t, "incomplete", out.Status)
|
||||
require.Len(t, out.Output, 1)
|
||||
require.Equal(t, "function_call", out.Output[0].Type)
|
||||
|
||||
@@ -532,7 +532,7 @@ func TestChatCompletionsResponseToResponses_DeepSeekReasoningOnlyFallsBackToMess
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false, nil)
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, nil, false, nil)
|
||||
|
||||
require.Len(t, out.Output, 2)
|
||||
require.Equal(t, "reasoning", out.Output[0].Type)
|
||||
@@ -566,7 +566,7 @@ func TestChatCompletionsResponseToResponses_DeepSeekReasoningToolCallDoesNotFall
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false, nil)
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, nil, false, nil)
|
||||
|
||||
require.Len(t, out.Output, 2)
|
||||
require.Equal(t, "reasoning", out.Output[0].Type)
|
||||
|
||||
@@ -50,6 +50,7 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions(
|
||||
return nil, fmt.Errorf("resolve responses tools: %w", err)
|
||||
}
|
||||
customTools := apicompat.CustomToolNames(effectiveTools)
|
||||
functionTools := apicompat.FunctionToolNames(effectiveTools)
|
||||
toolSearch := apicompat.HasToolSearchTool(effectiveTools)
|
||||
namespaceTools := apicompat.NamespaceToolNames(effectiveTools)
|
||||
|
||||
@@ -120,9 +121,9 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions(
|
||||
}
|
||||
|
||||
if clientStream {
|
||||
return s.streamChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, namespaceTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
return s.streamChatCompletionsAsResponses(c, resp, originalModel, customTools, functionTools, toolSearch, namespaceTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
}
|
||||
return s.bufferChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, namespaceTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
return s.bufferChatCompletionsAsResponses(c, resp, originalModel, customTools, functionTools, toolSearch, namespaceTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses(
|
||||
@@ -130,6 +131,7 @@ func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses(
|
||||
resp *http.Response,
|
||||
originalModel string,
|
||||
customTools map[string]bool,
|
||||
functionTools map[string]bool,
|
||||
toolSearch bool,
|
||||
namespaceTools map[string]apicompat.NamespacedToolName,
|
||||
billingModel string,
|
||||
@@ -143,7 +145,7 @@ func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responsesResp := apicompat.ChatCompletionsResponseToResponses(ccResp, originalModel, customTools, toolSearch, namespaceTools)
|
||||
responsesResp := apicompat.ChatCompletionsResponseToResponses(ccResp, originalModel, customTools, functionTools, toolSearch, namespaceTools)
|
||||
s.cacheReasoningItemsFromOutput(responsesResp.Output)
|
||||
|
||||
if s.responseHeaderFilter != nil {
|
||||
@@ -169,6 +171,7 @@ func (s *OpenAIGatewayService) streamChatCompletionsAsResponses(
|
||||
resp *http.Response,
|
||||
originalModel string,
|
||||
customTools map[string]bool,
|
||||
functionTools map[string]bool,
|
||||
toolSearch bool,
|
||||
namespaceTools map[string]apicompat.NamespacedToolName,
|
||||
billingModel string,
|
||||
@@ -182,6 +185,7 @@ func (s *OpenAIGatewayService) streamChatCompletionsAsResponses(
|
||||
|
||||
state := apicompat.NewChatCompletionsToResponsesStreamState(originalModel)
|
||||
state.CustomTools = customTools
|
||||
state.FunctionTools = functionTools
|
||||
state.ToolSearchDeclared = toolSearch
|
||||
state.NamespaceTools = namespaceTools
|
||||
clientDisconnected := false
|
||||
|
||||
Reference in New Issue
Block a user