mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 20:12:39 +08:00
fix(openai): 保留 Responses Lite namespace 调用
This commit is contained in:
@@ -78,8 +78,9 @@ func shouldStripOpenAIResponsesInputNamespaces(account *Account, transport OpenA
|
||||
// - compact 端点的 schema 不含该字段,携带即 400 `Unknown parameter:
|
||||
// input[N].namespace`(issue #4761 正文),故 compact 一律清理。
|
||||
// - API Key 出口默认按标准 Responses API 处理并清理该字段;但当请求本身声明
|
||||
// namespace 工具时,上游显然使用了 namespace 扩展,此时必须保留调用项上的
|
||||
// namespace,否则声明与历史调用会失配并触发 Missing namespace。
|
||||
// namespace 工具时(包括 Responses Lite 的 input[].additional_tools),上游显然
|
||||
// 使用了 namespace 扩展,此时必须保留调用项上的 namespace,否则声明与历史
|
||||
// 调用会失配并触发 Missing namespace。
|
||||
// - 摊平模式下调用项已被改写成平名,残留 namespace 指向的声明已不存在,一律清理。
|
||||
func shouldKeepOpenAIResponsesToolCallNamespaces(
|
||||
account *Account,
|
||||
@@ -104,13 +105,39 @@ func shouldKeepOpenAIResponsesToolCallNamespaces(
|
||||
}
|
||||
|
||||
func hasOpenAIResponsesNamespaceToolDeclaration(body []byte) bool {
|
||||
tools := gjson.GetBytes(body, "tools")
|
||||
if !tools.IsArray() {
|
||||
hasNamespaceTool := func(tools gjson.Result) bool {
|
||||
if !tools.IsArray() {
|
||||
return false
|
||||
}
|
||||
found := false
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(tool.Get("type").String()), "namespace") {
|
||||
found = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
if hasNamespaceTool(gjson.GetBytes(body, "tools")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Responses Lite moves private namespace declarations out of top-level
|
||||
// tools and into an input.additional_tools carrier. API-key requests are
|
||||
// intentionally not rewritten by normalizeOpenAIResponsesLiteTools, so the
|
||||
// declaration can arrive here only in this form.
|
||||
input := gjson.GetBytes(body, "input")
|
||||
if !input.IsArray() {
|
||||
return false
|
||||
}
|
||||
found := false
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(tool.Get("type").String()), "namespace") {
|
||||
input.ForEach(func(_, item gjson.Result) bool {
|
||||
if !strings.EqualFold(strings.TrimSpace(item.Get("type").String()), "additional_tools") {
|
||||
return true
|
||||
}
|
||||
if hasNamespaceTool(item.Get("tools")) {
|
||||
found = true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -89,6 +89,41 @@ func TestOpenAIGatewayService_APIKeyPreservesDeclaredNamespaceToolCalls(t *testi
|
||||
require.False(t, gjson.GetBytes(forwarded, "input.1.namespace").Exists())
|
||||
}
|
||||
|
||||
// Responses Lite 把 namespace 声明放在 input.additional_tools,而不是顶层 tools。
|
||||
// OpenAI API Key 走该请求形态时也必须保留历史 function_call.namespace。
|
||||
func TestOpenAIGatewayService_APIKeyPreservesLiteDeclaredNamespaceToolCalls(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"model":"gpt-5.6-terra",
|
||||
"stream":false,
|
||||
"input":[
|
||||
{"type":"function_call","namespace":"mcp__cua_repl","name":"js","call_id":"call_js","arguments":"{}"},
|
||||
{"type":"message","role":"user","namespace":"leftover","content":[{"type":"input_text","text":"hello"}]},
|
||||
{"type":"additional_tools","role":"developer","tools":[
|
||||
{"type":"namespace","name":"mcp__cua_repl","tools":[{"type":"function","name":"js","parameters":{"type":"object"}}]}
|
||||
]}
|
||||
]
|
||||
}`)
|
||||
upstream := &httpUpstreamRecorder{responses: []*http.Response{
|
||||
newOpenAIRejectedFieldTestResponse(http.StatusOK, namespaceForwardOKResponse),
|
||||
}}
|
||||
c := newOpenAIRejectedFieldTestContext(body)
|
||||
c.Request.Header.Set(responsesLiteHeader, "true")
|
||||
|
||||
result, err := newOpenAIRejectedFieldTestService(upstream).Forward(
|
||||
context.Background(), c, newOpenAIRejectedFieldTestAccount(), body,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, upstream.bodies, 1)
|
||||
forwarded := upstream.bodies[0]
|
||||
|
||||
require.True(t, gjson.GetBytes(forwarded, `input.#(type=="additional_tools").tools.#(type=="namespace")`).Exists())
|
||||
require.Equal(t, "mcp__cua_repl", gjson.GetBytes(forwarded, "input.0.namespace").String())
|
||||
require.Equal(t, "js", gjson.GetBytes(forwarded, "input.0.name").String())
|
||||
require.False(t, gjson.GetBytes(forwarded, "input.1.namespace").Exists())
|
||||
}
|
||||
|
||||
// compact 端点 schema 更窄:input[].namespace 会 400 Unknown parameter(issue #4761),
|
||||
// 且没有证据表明它接受 namespace 工具声明。compact 只做历史摘要、不需要模型寻址工具,
|
||||
// 因此保持既有的摊平 + 全量清理行为,不随默认值翻转扩大风险面。
|
||||
|
||||
@@ -98,6 +98,9 @@ func TestShouldKeepOpenAIResponsesToolCallNamespaces(t *testing.T) {
|
||||
{name: "apikey_without_namespace_tool_strips", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, want: false},
|
||||
{name: "apikey_with_namespace_tool_keeps", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, body: []byte(`{"tools":[{"type":"namespace","name":"mcp__codex_app","tools":[]}]}`), want: true},
|
||||
{name: "apikey_with_mixed_case_namespace_tool_keeps", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, body: []byte(`{"tools":[{"type":" Namespace ","name":"mcp__codex_app","tools":[]}]}`), want: true},
|
||||
{name: "apikey_with_lite_additional_namespace_tool_keeps", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, body: []byte(`{"input":[{"type":"additional_tools","role":"developer","tools":[{"type":"namespace","name":"mcp__cua_repl","tools":[{"type":"function","name":"js"}]}]}]}`), want: true},
|
||||
{name: "apikey_with_mixed_case_lite_carrier_keeps", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, body: []byte(`{"input":[{"type":" Additional_Tools ","tools":[{"type":" Namespace ","name":"mcp__cua_repl","tools":[]}]}]}`), want: true},
|
||||
{name: "apikey_with_lite_function_only_strips", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, body: []byte(`{"input":[{"type":"additional_tools","tools":[{"type":"function","name":"js","namespace":"mcp__cua_repl"}]}]}`), want: false},
|
||||
{name: "apikey_function_tool_with_namespace_field_strips", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, body: []byte(`{"tools":[{"type":"function","name":"automation_update","namespace":"mcp__codex_app"}]}`), want: false},
|
||||
{name: "apikey_compact_with_namespace_tool_strips", account: apiKey, transport: OpenAIUpstreamTransportHTTPSSE, compactPath: true, body: []byte(`{"tools":[{"type":"namespace","name":"mcp__codex_app","tools":[]}]}`), want: false},
|
||||
{name: "setup_token_keeps", account: setupToken, transport: OpenAIUpstreamTransportHTTPSSE, want: true},
|
||||
|
||||
Reference in New Issue
Block a user