mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
fix: Anthropic tool schema 转 Responses API 时补充缺失的 properties 字段
当 Claude Code 发来的 MCP tool 的 input_schema 为 {"type":"object"} 且缺少
properties 字段时,OpenAI Codex 后端会拒绝并报错:
Invalid schema for function '...': object schema missing properties.
新增 normalizeToolParameters 函数,在 convertAnthropicToolsToResponses 中
对每个 tool 的 InputSchema 做规范化处理后再赋给 Parameters。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1008,3 +1008,114 @@ func TestAnthropicToResponses_ImageEmptyMediaType(t *testing.T) {
|
|||||||
// Should default to image/png when media_type is empty.
|
// Should default to image/png when media_type is empty.
|
||||||
assert.Equal(t, "data:image/png;base64,iVBOR", parts[0].ImageURL)
|
assert.Equal(t, "data:image/png;base64,iVBOR", parts[0].ImageURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// normalizeToolParameters tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestNormalizeToolParameters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input json.RawMessage
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil input",
|
||||||
|
input: nil,
|
||||||
|
expected: `{"type":"object","properties":{}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: json.RawMessage(``),
|
||||||
|
expected: `{"type":"object","properties":{}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null input",
|
||||||
|
input: json.RawMessage(`null`),
|
||||||
|
expected: `{"type":"object","properties":{}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object without properties",
|
||||||
|
input: json.RawMessage(`{"type":"object"}`),
|
||||||
|
expected: `{"type":"object","properties":{}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object with properties",
|
||||||
|
input: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}}}`),
|
||||||
|
expected: `{"type":"object","properties":{"city":{"type":"string"}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-object type",
|
||||||
|
input: json.RawMessage(`{"type":"string"}`),
|
||||||
|
expected: `{"type":"string"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object with additional fields preserved",
|
||||||
|
input: json.RawMessage(`{"type":"object","required":["name"]}`),
|
||||||
|
expected: `{"type":"object","required":["name"],"properties":{}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid JSON passthrough",
|
||||||
|
input: json.RawMessage(`not json`),
|
||||||
|
expected: `not json`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := normalizeToolParameters(tt.input)
|
||||||
|
if tt.name == "invalid JSON passthrough" {
|
||||||
|
assert.Equal(t, tt.expected, string(result))
|
||||||
|
} else {
|
||||||
|
assert.JSONEq(t, tt.expected, string(result))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnthropicToResponses_ToolWithoutProperties(t *testing.T) {
|
||||||
|
req := &AnthropicRequest{
|
||||||
|
Model: "gpt-5.2",
|
||||||
|
MaxTokens: 1024,
|
||||||
|
Messages: []AnthropicMessage{
|
||||||
|
{Role: "user", Content: json.RawMessage(`"Hello"`)},
|
||||||
|
},
|
||||||
|
Tools: []AnthropicTool{
|
||||||
|
{Name: "mcp__pencil__get_style_guide_tags", Description: "Get style tags", InputSchema: json.RawMessage(`{"type":"object"}`)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := AnthropicToResponses(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, resp.Tools, 1)
|
||||||
|
assert.Equal(t, "function", resp.Tools[0].Type)
|
||||||
|
assert.Equal(t, "mcp__pencil__get_style_guide_tags", resp.Tools[0].Name)
|
||||||
|
|
||||||
|
// Parameters must have "properties" field after normalization.
|
||||||
|
var params map[string]json.RawMessage
|
||||||
|
require.NoError(t, json.Unmarshal(resp.Tools[0].Parameters, ¶ms))
|
||||||
|
assert.Contains(t, params, "properties")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnthropicToResponses_ToolWithNilSchema(t *testing.T) {
|
||||||
|
req := &AnthropicRequest{
|
||||||
|
Model: "gpt-5.2",
|
||||||
|
MaxTokens: 1024,
|
||||||
|
Messages: []AnthropicMessage{
|
||||||
|
{Role: "user", Content: json.RawMessage(`"Hello"`)},
|
||||||
|
},
|
||||||
|
Tools: []AnthropicTool{
|
||||||
|
{Name: "simple_tool", Description: "A tool"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := AnthropicToResponses(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, resp.Tools, 1)
|
||||||
|
var params map[string]json.RawMessage
|
||||||
|
require.NoError(t, json.Unmarshal(resp.Tools[0].Parameters, ¶ms))
|
||||||
|
assert.JSONEq(t, `"object"`, string(params["type"]))
|
||||||
|
assert.JSONEq(t, `{}`, string(params["properties"]))
|
||||||
|
}
|
||||||
|
|||||||
@@ -409,8 +409,41 @@ func convertAnthropicToolsToResponses(tools []AnthropicTool) []ResponsesTool {
|
|||||||
Type: "function",
|
Type: "function",
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Description: t.Description,
|
Description: t.Description,
|
||||||
Parameters: t.InputSchema,
|
Parameters: normalizeToolParameters(t.InputSchema),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeToolParameters ensures the tool parameter schema is valid for
|
||||||
|
// OpenAI's Responses API, which requires "properties" on object schemas.
|
||||||
|
//
|
||||||
|
// - nil/empty → {"type":"object","properties":{}}
|
||||||
|
// - type=object without properties → adds "properties": {}
|
||||||
|
// - otherwise → returned unchanged
|
||||||
|
func normalizeToolParameters(schema json.RawMessage) json.RawMessage {
|
||||||
|
if len(schema) == 0 || string(schema) == "null" {
|
||||||
|
return json.RawMessage(`{"type":"object","properties":{}}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
var m map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(schema, &m); err != nil {
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
typ, _ := m["type"]
|
||||||
|
if string(typ) != `"object"` {
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m["properties"]; ok {
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
m["properties"] = json.RawMessage(`{}`)
|
||||||
|
out, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user