返回 DeepSeek-Reasonix
schema_dialect.go
根目录 / internal / provider / schema_dialect.go
1 package provider
2
3 import (
4 "bytes"
5 "encoding/json"
6 "net/url"
7 "strings"
8 )
9
10 // IsMiMoEndpoint reports whether rawURL points at an official Xiaomi MiMo API
11 // host, including the regional token-plan subdomains. The bare apex is rejected
12 // because it is not an API endpoint.
13 func IsMiMoEndpoint(rawURL string) bool {
14 u, err := url.Parse(rawURL)
15 if err != nil {
16 return false
17 }
18 host := strings.ToLower(u.Hostname())
19 return host != "xiaomimimo.com" && strings.HasSuffix(host, ".xiaomimimo.com")
20 }
21
22 // NormalizeLegacyTupleItemsForDraft202012 rewrites only the pre-2020-12 tuple
23 // keywords in a JSON Schema. It is intentionally separate from
24 // CanonicalizeSchema: provider implementations must opt in only after the target
25 // endpoint's schema dialect is known, so other vendors keep their original tool
26 // schema bytes and cache prefixes. A schema that needs no rewrite is returned
27 // with its input bytes unchanged, byte for byte.
28 //
29 // There is deliberately no memoization: MCP schemas are externally supplied
30 // bytes, and a process-global cache keyed by them would accumulate without
31 // bound across projects, sessions, and server reconnects in a long-lived
32 // desktop process. The lexical gate below keeps the common no-op case at zero
33 // parses, and the rare legacy-tuple schema is cheap to re-convert per request.
34 func NormalizeLegacyTupleItemsForDraft202012(raw json.RawMessage) json.RawMessage {
35 if len(raw) == 0 {
36 return raw
37 }
38 // Legacy tuple syntax cannot exist without an "items" keyword; the common
39 // no-op case skips even the parse.
40 if !bytes.Contains(raw, []byte(`"items"`)) {
41 return raw
42 }
43 var schema any
44 if err := json.Unmarshal(raw, &schema); err != nil {
45 return raw
46 }
47 if _, documentChanged := normalizeDraft202012Schema(schema); !documentChanged {
48 return raw
49 }
50 out, err := json.Marshal(schema)
51 if err != nil {
52 return raw
53 }
54 return json.RawMessage(out)
55 }
56
57 // normalizeDraft202012Schema rewrites legacy tuple keywords in place. It
58 // returns two signals: resourceChanged reports conversions that belong to the
59 // CALLER's schema resource (the node itself plus descendants up to the next
60 // $schema boundary), documentChanged reports conversions anywhere in the
61 // subtree and drives reserialization.
62 //
63 // The distinction is what keeps dialect updates inside their own resource: an
64 // object carrying its own $schema is an independent schema resource, so its
65 // conversions update its own old-draft declaration and then stop — they must
66 // not mark the parent as changed, or a 2019-09 parent embedding a converting
67 // 2020-12 $defs resource would have its untouched declaration "upgraded" and
68 // the semantics of unrelated parent keywords silently changed.
69 func normalizeDraft202012Schema(value any) (resourceChanged, documentChanged bool) {
70 schema, ok := value.(map[string]any)
71 if !ok {
72 return false, false
73 }
74 // Classify the resource's own dialect BEFORE touching anything: for an
75 // unknown/custom $schema this code cannot know whether the dialect defines
76 // 2020-12 tuple semantics, and a partial rewrite (keywords converted,
77 // declaration kept) would be self-contradictory in the other direction.
78 // JSON Schema requires processors to switch modes per dialect or leave the
79 // resource alone — so the whole custom resource, subtree included, stays
80 // untouched. Known legacy drafts and 2020-12 itself (where array-form
81 // items is simply malformed input worth repairing) proceed.
82 decl, hasDecl := schema["$schema"].(string)
83 if hasDecl && !isLegacyJSONSchemaDialect(decl) && !isDraft202012Dialect(decl) {
84 return false, false
85 }
86 changed := false // within this resource, up to nested $schema boundaries
87 doc := false
88 visit := func(child any) {
89 childResource, childDocument := normalizeDraft202012Schema(child)
90 changed = changed || childResource
91 doc = doc || childDocument
92 }
93
94 for _, keyword := range []string{
95 "additionalItems", "additionalProperties", "contains", "contentSchema",
96 "else", "if", "items", "not", "propertyNames", "then",
97 "unevaluatedItems", "unevaluatedProperties",
98 } {
99 visit(schema[keyword])
100 }
101 for _, keyword := range []string{"allOf", "anyOf", "oneOf", "prefixItems"} {
102 if children, ok := schema[keyword].([]any); ok {
103 for _, child := range children {
104 visit(child)
105 }
106 }
107 }
108 for _, keyword := range []string{
109 "$defs", "definitions", "dependentSchemas", "patternProperties", "properties",
110 } {
111 if children, ok := schema[keyword].(map[string]any); ok {
112 for _, child := range children {
113 visit(child)
114 }
115 }
116 }
117 if dependencies, ok := schema["dependencies"].(map[string]any); ok {
118 for _, child := range dependencies {
119 visit(child)
120 }
121 }
122
123 if legacyItems, ok := schema["items"].([]any); ok {
124 for _, child := range legacyItems {
125 visit(child)
126 }
127 changed = true
128 delete(schema, "items")
129 if len(legacyItems) > 0 {
130 // Keep an explicit 2020-12 prefix if a malformed mixed-dialect schema
131 // contains both forms.
132 if _, exists := schema["prefixItems"]; !exists {
133 schema["prefixItems"] = legacyItems
134 }
135 }
136 if additional, exists := schema["additionalItems"]; exists {
137 delete(schema, "additionalItems")
138 if isSchemaObjectOrBool(additional) {
139 schema["items"] = additional
140 }
141 }
142 }
143
144 if changed {
145 doc = true
146 if hasDecl && isLegacyJSONSchemaDialect(decl) {
147 schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"
148 }
149 }
150 if hasDecl {
151 // Resource boundary: this resource's changes (and its declaration
152 // update) are settled here and must not leak into the parent.
153 return false, doc
154 }
155 return changed, doc
156 }
157
158 // isLegacyJSONSchemaDialect reports whether decl names a pre-2020-12 JSON
159 // Schema dialect. Unknown or custom dialect URIs make the whole resource
160 // off-limits — this normalizer only understands the official drafts' tuple
161 // semantics.
162 func isLegacyJSONSchemaDialect(decl string) bool {
163 switch normalizeDialectURI(decl) {
164 case "json-schema.org/schema",
165 "json-schema.org/draft-03/schema",
166 "json-schema.org/draft-04/schema",
167 "json-schema.org/draft-06/schema",
168 "json-schema.org/draft-07/schema",
169 "json-schema.org/draft/2019-09/schema":
170 return true
171 }
172 return false
173 }
174
175 // isDraft202012Dialect reports whether decl names the 2020-12 dialect itself.
176 func isDraft202012Dialect(decl string) bool {
177 return normalizeDialectURI(decl) == "json-schema.org/draft/2020-12/schema"
178 }
179
180 func normalizeDialectURI(decl string) string {
181 d := strings.TrimSuffix(strings.TrimSpace(decl), "#")
182 d = strings.TrimPrefix(d, "http://")
183 return strings.TrimPrefix(d, "https://")
184 }
185
186 func isSchemaObjectOrBool(value any) bool {
187 switch value.(type) {
188 case map[string]any, bool:
189 return true
190 default:
191 return false
192 }
193 }
194
194 lines GO