返回 DeepSeek-Reasonix
schema_canonicalize.go
根目录 / internal / provider / schema_canonicalize.go
1 package provider
2
3 import (
4 "encoding/json"
5 "sort"
6 )
7
8 // CanonicalizeSchema recursively stabilizes a JSON Schema so the same logical
9 // schema always produces the same byte representation.
10 func CanonicalizeSchema(raw json.RawMessage) json.RawMessage {
11 if len(raw) == 0 {
12 // A tool with no parameters (common for MCP tools) yields an empty
13 // schema. An empty json.RawMessage makes json.Marshal of the enclosing
14 // request fail ("unexpected end of JSON input") and bricks the whole
15 // provider; emit a strict OpenAI-compatible empty-object schema instead.
16 return json.RawMessage(`{"properties":{},"required":[],"type":"object"}`)
17 }
18 var v any
19 if err := json.Unmarshal(raw, &v); err != nil {
20 return raw
21 }
22 if v == nil {
23 // A nil RawMessage persists as JSON null in the MCP schema cache. Treat
24 // both forms as the same no-argument schema so old cache entries remain
25 // usable and never reach a strict provider as parameters: null.
26 return json.RawMessage(`{"properties":{},"required":[],"type":"object"}`)
27 }
28 canon := canonicalizeSchemaValue(v)
29 ensureRootObjectProperties(canon)
30 b, err := json.Marshal(canon)
31 if err != nil {
32 return raw
33 }
34 return json.RawMessage(b)
35 }
36
37 func ensureRootObjectProperties(v any) {
38 m, ok := v.(map[string]any)
39 if !ok {
40 return
41 }
42 if _, ok := m["type"]; !ok {
43 // MCP servers routinely omit the root type (or advertise a bare {}),
44 // while MCP and the Anthropic/OpenAI tool contracts require tool
45 // parameters to declare type "object". Tool arguments are always JSON
46 // objects, so the omission can only mean an object schema; make it
47 // explicit instead of letting validation quarantine a usable tool.
48 m["type"] = "object"
49 }
50 if m["type"] != "object" {
51 return
52 }
53 if _, ok := m["properties"]; !ok {
54 m["properties"] = map[string]any{}
55 }
56 // Relays that re-serialize schemas into typed structs emit an omitted
57 // required as null, which strict upstreams reject as "not of type array".
58 if _, ok := m["required"]; !ok {
59 m["required"] = []any{}
60 }
61 }
62
63 func canonicalizeSchemaValue(v any) any {
64 return canonicalizeSchemaObject(v)
65 }
66
67 func canonicalizeSchemaObject(v any) any {
68 switch val := v.(type) {
69 case map[string]any:
70 for k, inner := range val {
71 switch k {
72 case "properties", "patternProperties", "$defs", "definitions", "dependentSchemas":
73 val[k] = canonicalizeNamedSchemas(inner)
74 case "dependentRequired":
75 val[k] = canonicalizeDependentRequired(inner)
76 default:
77 val[k] = canonicalizeSchemaObject(inner)
78 }
79 }
80 if req, ok := val["required"]; ok {
81 if arr, ok := req.([]any); ok {
82 sortSchemaArray(arr)
83 } else {
84 // Some MCP servers emit OpenAPI-style property metadata such as
85 // {"required": true}. OpenAI-compatible function schemas require
86 // JSON Schema's array form; dropping the invalid value keeps the
87 // whole tool list from being rejected with HTTP 400.
88 delete(val, "required")
89 }
90 }
91 if dr, ok := val["dependentRequired"]; ok && !isJSONObject(dr) {
92 delete(val, "dependentRequired")
93 }
94 return val
95 case []any:
96 for i, elem := range val {
97 val[i] = canonicalizeSchemaObject(elem)
98 }
99 return val
100 default:
101 return v
102 }
103 }
104
105 func canonicalizeNamedSchemas(v any) any {
106 m, ok := v.(map[string]any)
107 if !ok {
108 return canonicalizeSchemaObject(v)
109 }
110 for name, schema := range m {
111 m[name] = canonicalizeSchemaObject(schema)
112 }
113 return m
114 }
115
116 func canonicalizeDependentRequired(v any) any {
117 m, ok := v.(map[string]any)
118 if !ok {
119 return v
120 }
121 for key, inner := range m {
122 if arr, ok := inner.([]any); ok {
123 sortSchemaArray(arr)
124 } else {
125 delete(m, key)
126 }
127 }
128 return m
129 }
130
131 func isJSONObject(v any) bool {
132 _, ok := v.(map[string]any)
133 return ok
134 }
135
136 func sortSchemaArray(arr []any) {
137 sort.SliceStable(arr, func(i, j int) bool {
138 return schemaJSONString(arr[i]) < schemaJSONString(arr[j])
139 })
140 }
141
142 func schemaJSONString(v any) string {
143 b, _ := json.Marshal(v)
144 return string(b)
145 }
146
146 lines GO