// Package wire/schema.go - Tool Schema 预处理工具 // // 精妙之处(CLEVER): 在 wire 层统一处理 schema 兼容性问题, // 而非在每个 provider 各自实现-- // $ref 展开逻辑复杂,集中实现一次,所有有 bug 的 provider 共用. // // 背景(BUGFIX): MiniMax 直连和 OpenRouter→Gemini 有 $ref 双重序列化 bug: // 工具 schema 里用 $ref 引用 $defs 时,返回的 tool input 中被引用字段是 // JSON 字符串而非 object,导致静默数据损坏. // 在发送前展开 $ref,彻底规避此类 provider 端的序列化缺陷. // // 替代方案:<各 provider 各自处理 $ref 展开> - 否决: // $ref 递归展开有循环引用检测等复杂逻辑,各自实现容易出现不一致 bug, // 且每次新增有问题的 provider 都需重复实现. // // AdaptSchema: 各 provider 对 JSON Schema 约束支持不一, // 消费者写标准 schema,引擎自动裁剪不支持的约束. // // ValidateSchemaComplexity: OpenAI strict 有隐藏嵌套/属性数/enum 上限, // 超出时仅返回 400 + 晦涩错误;引擎提前检测,给出可读错误. package wire import ( "encoding/json" "errors" "fmt" "strings" ) // ErrCircularRef 表示 schema 中存在循环引用(A → B → A). // 循环引用无法内联展开,调用方应选择保留原始 schema 或报错. var ErrCircularRef = errors.New("wire/schema: circular $ref detected") // ErrExternalRef 表示 schema 中包含外部 URL $ref(非 #/$defs/ 前缀). // 外部引用需要网络请求解析,超出本函数职责范围. var ErrExternalRef = errors.New("wire/schema: external $ref not supported") // DereferenceSchema 展开 schema 中所有内部 $ref 引用. // // 精妙之处(CLEVER): 两阶段处理-- // 1. 收集 $defs 中所有定义(名称 → JSON 对象) // 2. 递归遍历 schema,遇到 $ref: "#/$defs/Foo" 时内联替换 // // 展开后删除顶层 $defs 和所有残余 $ref 字段, // 保证输出对不支持 $ref 的 provider 完全透明. // // 不含 $ref 的 schema 原样返回(零开销路径). // 只处理内部引用(#/$defs/),外部 URL $ref 返回 ErrExternalRef. // 检测循环引用(A → B → A),返回 ErrCircularRef. // // 替代方案:<先 Unmarshal 到 any 再处理> - 否决: // any 路径会丢失 JSON 数字精度(int64 大数变 float64), // 直接操作 map[string]json.RawMessage 保留原始字节. func DereferenceSchema(schema json.RawMessage) (json.RawMessage, error) { if len(schema) == 0 { return schema, nil } // 快速路径:没有 $ref 则原样返回,避免不必要的 JSON 解析开销. if !strings.Contains(string(schema), `"$ref"`) { return schema, nil } // 解析顶层对象,收集 $defs var top map[string]json.RawMessage if err := json.Unmarshal(schema, &top); err != nil { // 不是对象(如数组):没有 $defs,直接返回原值 return schema, nil } // 收集 $defs(名称 → 原始 JSON) defs := make(map[string]json.RawMessage) if defsRaw, ok := top["$defs"]; ok { var defsMap map[string]json.RawMessage if err := json.Unmarshal(defsRaw, &defsMap); err != nil { return nil, fmt.Errorf("wire/schema: parse $defs: %w", err) } for k, v := range defsMap { defs[k] = v } } // 递归展开,使用 visiting 集合检测循环引用 visiting := make(map[string]bool) result, err := derefValue(top, defs, visiting) if err != nil { return nil, err } // 删除顶层 $defs(已内联,无需保留) delete(result, "$defs") out, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("wire/schema: marshal result: %w", err) } return out, nil } // derefValue 递归展开一个 map(schema 对象)中的所有 $ref. // // visiting 记录当前递归栈中正在展开的 def 名称,用于检测循环引用. // 精妙之处(CLEVER): visiting 是深度优先栈,不是全局已访问集-- // A→B→C 完成后,若 A→D→B 也存在,B 应该可以正常展开(不是循环). // 因此每次进入 def 前 push,退出后 pop(使用 defer 确保 pop). func derefValue(obj map[string]json.RawMessage, defs map[string]json.RawMessage, visiting map[string]bool) (map[string]json.RawMessage, error) { // 检查当前对象是否为 $ref if refRaw, hasRef := obj["$ref"]; hasRef { var refStr string if err := json.Unmarshal(refRaw, &refStr); err != nil { return nil, fmt.Errorf("wire/schema: parse $ref value: %w", err) } // 外部 $ref 检查 if !strings.HasPrefix(refStr, "#/$defs/") { return nil, ErrExternalRef } defName := strings.TrimPrefix(refStr, "#/$defs/") // 循环引用检测 if visiting[defName] { return nil, ErrCircularRef } defRaw, ok := defs[defName] if !ok { return nil, fmt.Errorf("wire/schema: $ref %q not found in $defs", refStr) } // 解析被引用定义 var defObj map[string]json.RawMessage if err := json.Unmarshal(defRaw, &defObj); err != nil { return nil, fmt.Errorf("wire/schema: parse def %q: %w", defName, err) } // 进入循环引用检测栈 visiting[defName] = true defer func() { delete(visiting, defName) }() // 递归展开被引用的定义 expanded, err := derefValue(defObj, defs, visiting) if err != nil { return nil, err } // 将 $ref 所在对象中的其他字段(如 description)与展开结果合并 // 精妙之处(CLEVER): JSON Schema 允许 $ref 与 description 等字段并存, // 合并时被引用定义的字段优先($ref 语义);调用方的额外字段保留. merged := make(map[string]json.RawMessage, len(expanded)+len(obj)) for k, v := range obj { if k != "$ref" { merged[k] = v } } for k, v := range expanded { merged[k] = v // 被引用定义覆盖 } return merged, nil } // 非 $ref 对象:递归处理每个字段的值 result := make(map[string]json.RawMessage, len(obj)) for k, v := range obj { expanded, err := derefAnyValue(v, defs, visiting) if err != nil { return nil, fmt.Errorf("wire/schema: field %q: %w", k, err) } result[k] = expanded } return result, nil } // ───────────────────────────────────────────────────────────────────────────── // CAP-6: AdaptSchema - Provider 约束裁剪 // ───────────────────────────────────────────────────────────────────────────── // adaptAnthropicConstraints 列举 Anthropic 不支持的 JSON Schema 数值/字符串约束. // // 精妙之处(CLEVER): Anthropic API 在遇到这些字段时不报错,而是静默忽略-- // 但消费者可能误以为约束生效(如 minimum=1 被模型忽略导致输入为 0). // 裁剪这些字段并将其含义转移到 description,确保模型"读懂"约束而非依赖 schema 机制. // // 官方文档(2026-04): Anthropic tool use JSON Schema 不支持数值范围,字符串长度, // multipleOf 等验证关键字;minItems 仅 0/1 有意义(0=可选数组,1=非空数组). var adaptAnthropicConstraints = []string{ "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "minLength", "maxLength", "pattern", } // adaptOpenAIStrictConstraints 是 OpenAI strict 模式不支持的组合关键字列表. // // OpenAI strict 模式要求 schema 是"structurally valid JSON Schema subset": // - 不支持 allOf/anyOf/oneOf/not(条件组合) // - 不支持 if/then/else(条件逻辑) // - 不支持 contains/prefixItems/unevaluatedItems(数组高级约束) // // 替代方案:<直接透传让 OpenAI 报错> - 否决:OpenAI 错误信息含糊("Invalid schema"), // 在引擎层提前裁剪并给出明确 warning 比让消费者追 400 错误更友好. // // 注意:anyOf/oneOf 在非 strict 模式下受支持;此列表仅适用于 strict=true 场景. var adaptOpenAIStrictConstraints = []string{ "allOf", "not", "if", "then", "else", "contains", "prefixItems", "unevaluatedItems", } // AdaptSchema 裁剪 schema 中指定 provider 不支持的 JSON Schema 约束. // // provider 参数: // - "anthropic" - 移除数值/字符串约束关键字,将约束信息写入 description // - "openai-strict" - 移除 allOf/not/if/then/else 等条件组合关键字 // - 其他("minimax"/"openrouter"/"openai"/"gemini" 等)- 原样返回(probe 确认全支持) // // 精妙之处(CLEVER): 裁剪 Anthropic 约束时将其写入 description,而非静默丢弃-- // 例如 minimum=1, maximum=100 变成 description 追加 "(range: 1-100)". // 模型在没有 schema validation 时依然能"看到"约束,这比静默裁剪更安全. // 原方案:<静默丢弃约束> - 否决:模型可能生成不合法的值,且无法从 description 中感知约束存在. // // 替代方案:<返回 error 让消费者处理> - 否决: // 约束裁剪是引擎自动化兼容的核心职责,报错会破坏"消费者写标准 schema"的体验. // 无法裁剪的情况(JSON 解析失败)静默 fallback 到原值,不影响请求发出. func AdaptSchema(schema json.RawMessage, provider string) json.RawMessage { if len(schema) == 0 { return schema } switch provider { case "anthropic": adapted, err := adaptSchemaAnthropicRecursive(schema) if err != nil { return schema // fallback:保留原始 schema,让 provider 决定如何处理 } return adapted case "openai-strict": adapted, err := adaptSchemaRemoveKeys(schema, adaptOpenAIStrictConstraints) if err != nil { return schema } return adapted default: // minimax/openrouter/openai/gemini 等:probe 确认全支持,原样返回 return schema } } // adaptSchemaAnthropicRecursive 递归裁剪 Anthropic 不支持的约束, // 并将数值/字符串范围约束写入 description 字段. func adaptSchemaAnthropicRecursive(raw json.RawMessage) (json.RawMessage, error) { if len(raw) == 0 { return raw, nil } switch raw[0] { case '{': var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { return raw, nil } // 收集需要裁剪的约束值(用于追加到 description) var descParts []string for _, key := range adaptAnthropicConstraints { if val, ok := obj[key]; ok { var v any if err := json.Unmarshal(val, &v); err == nil { descParts = append(descParts, fmt.Sprintf("%s=%v", key, v)) } delete(obj, key) } } // minItems:仅保留 0/1(Anthropic 语义) if miRaw, ok := obj["minItems"]; ok { var mi int if err := json.Unmarshal(miRaw, &mi); err == nil && mi > 1 { // 将 minItems > 1 转移到 description descParts = append(descParts, fmt.Sprintf("minItems=%d", mi)) // 保留 minItems=1(表示非空数组) b, _ := json.Marshal(1) obj["minItems"] = b } } // 精妙之处(CLEVER): 将裁剪的约束追加到 description,而非静默丢弃-- // 模型在缺少 schema validation 时仍能通过 description 感知约束. // 格式: 原有 description + " (constraint: key=val, ...)" // 若原无 description,新建一条纯约束说明. if len(descParts) > 0 { constraintNote := "(constraint: " + strings.Join(descParts, ", ") + ")" if descRaw, ok := obj["description"]; ok { var existing string if err := json.Unmarshal(descRaw, &existing); err == nil { existing = existing + " " + constraintNote b, _ := json.Marshal(existing) obj["description"] = b } } else { b, _ := json.Marshal(constraintNote) obj["description"] = b } } // 递归处理子 schema. // 精妙之处(CLEVER): 只递归到"真正是子 schema 的字段",而不是所有子 value-- // 否则会把 properties 也当作 schema 对待,properties 的键(如 "pattern", "minimum") // 会被当作约束关键字删除,导致工具 schema 损坏(例如 Glob 工具有 "pattern" 属性会被误删). // 替代方案:<盲目递归所有子 value> - 否决:如上所述,会破坏属性名碰巧等于 JSON Schema 关键字的 schema. // // 对 properties/patternProperties/$defs/definitions:递归每个 value(key 是属性名,不递归) for _, container := range []string{"properties", "patternProperties", "$defs", "definitions"} { if raw, ok := obj[container]; ok { var subObj map[string]json.RawMessage if err := json.Unmarshal(raw, &subObj); err == nil { for k, v := range subObj { adapted, err := adaptSchemaAnthropicRecursive(v) if err != nil { return nil, fmt.Errorf("%s.%q: %w", container, k, err) } subObj[k] = adapted } if marshaled, err := json.Marshal(subObj); err == nil { obj[container] = marshaled } } } } // 对 items/additionalProperties/contains/propertyNames/if/then/else/not:值本身是 schema for _, schemaField := range []string{"items", "additionalProperties", "contains", "propertyNames", "if", "then", "else", "not"} { if raw, ok := obj[schemaField]; ok && len(raw) > 0 && raw[0] == '{' { adapted, err := adaptSchemaAnthropicRecursive(raw) if err != nil { return nil, fmt.Errorf("%s: %w", schemaField, err) } obj[schemaField] = adapted } } // 对 allOf/anyOf/oneOf/prefixItems:值是 schema 数组 for _, schemaArray := range []string{"allOf", "anyOf", "oneOf", "prefixItems"} { if raw, ok := obj[schemaArray]; ok && len(raw) > 0 && raw[0] == '[' { adapted, err := adaptSchemaAnthropicRecursive(raw) if err != nil { return nil, fmt.Errorf("%s: %w", schemaArray, err) } obj[schemaArray] = adapted } } out, err := json.Marshal(obj) if err != nil { return nil, err } return out, nil case '[': var arr []json.RawMessage if err := json.Unmarshal(raw, &arr); err != nil { return raw, nil } for i, elem := range arr { adapted, err := adaptSchemaAnthropicRecursive(elem) if err != nil { return nil, fmt.Errorf("index %d: %w", i, err) } arr[i] = adapted } out, err := json.Marshal(arr) if err != nil { return nil, err } return out, nil default: return raw, nil } } // adaptSchemaRemoveKeys 递归删除 schema 中指定的关键字(用于 OpenAI strict 模式). func adaptSchemaRemoveKeys(raw json.RawMessage, keys []string) (json.RawMessage, error) { if len(raw) == 0 { return raw, nil } switch raw[0] { case '{': var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { return raw, nil } for _, k := range keys { delete(obj, k) } for k, v := range obj { adapted, err := adaptSchemaRemoveKeys(v, keys) if err != nil { return nil, fmt.Errorf("field %q: %w", k, err) } obj[k] = adapted } out, err := json.Marshal(obj) if err != nil { return nil, err } return out, nil case '[': var arr []json.RawMessage if err := json.Unmarshal(raw, &arr); err != nil { return raw, nil } for i, elem := range arr { adapted, err := adaptSchemaRemoveKeys(elem, keys) if err != nil { return nil, fmt.Errorf("index %d: %w", i, err) } arr[i] = adapted } out, err := json.Marshal(arr) if err != nil { return nil, err } return out, nil default: return raw, nil } } // ───────────────────────────────────────────────────────────────────────────── // CAP-8: ValidateSchemaComplexity - OpenAI strict 复杂度保护 // ───────────────────────────────────────────────────────────────────────────── // OpenAI strict 模式的隐藏复杂度上限(来源:OpenAI Platform 文档 2026-04). const ( // openAIMaxNestingDepth 是 OpenAI strict 模式允许的最大嵌套深度. // 超出时 API 返回 400 "schema exceeds maximum nesting depth". openAIMaxNestingDepth = 10 // openAIMaxTotalProperties 是 schema 中所有层级属性总数的上限. // 超出时 API 返回 400 含糊错误;文档值 5000(2026-04). openAIMaxTotalProperties = 5000 // openAIMaxEnumValues 是单个 enum 数组允许的最大元素数. // 超出时 API 返回 400;文档值 1000(2026-04). openAIMaxEnumValues = 1000 ) // SchemaComplexityError 是 schema 复杂度超限错误,包含超限维度和实际值. // // 精妙之处(CLEVER): 自定义错误类型而非 fmt.Errorf-- // 调用方可以 type-assert 到 *SchemaComplexityError, // 获取结构化的 Dimension/Actual/Limit 用于监控打点或自动裁剪. // 替代方案:<直接 fmt.Errorf> - 否决:调用方只能解析错误字符串,脆弱. type SchemaComplexityError struct { // Dimension 是超限维度("nesting_depth" / "total_properties" / "enum_values"). Dimension string // Actual 是实际测量值. Actual int // Limit 是允许的上限. Limit int } func (e *SchemaComplexityError) Error() string { return fmt.Sprintf("schema %s %d exceeds OpenAI limit of %d", e.Dimension, e.Actual, e.Limit) } // ValidateSchemaComplexity 验证 schema 是否满足指定 provider 的复杂度限制. // // provider 参数: // - "openai-strict" - 检查嵌套深度(≤10),总属性数(≤5000),enum 值数(≤1000) // - 其他 - 当前无已知硬性限制,始终返回 nil // // 返回 *SchemaComplexityError(可 type-assert)或 nil. // // 精妙之处(CLEVER): 三项检查独立运行,返回第一个超限的维度-- // 实践中嵌套深度是最常触发的限制,优先报告;总属性数次之;enum 值数最后. // 消费者修复第一个问题后重新调用,逐一解决,避免一次 error 掩盖多个问题. // 替代方案:<一次返回所有超限维度> - 否决:接口复杂([]error),消费者处理成本高. func ValidateSchemaComplexity(schema json.RawMessage, provider string) error { if len(schema) == 0 || provider != "openai-strict" { return nil } // 检查嵌套深度 depth := schemaMaxDepth(schema, 0) if depth > openAIMaxNestingDepth { return &SchemaComplexityError{ Dimension: "nesting_depth", Actual: depth, Limit: openAIMaxNestingDepth, } } // 检查总属性数 total := schemaTotalProperties(schema) if total > openAIMaxTotalProperties { return &SchemaComplexityError{ Dimension: "total_properties", Actual: total, Limit: openAIMaxTotalProperties, } } // 检查 enum 值数(任一 enum 超限即报错) if maxEnum := schemaMaxEnumLen(schema); maxEnum > openAIMaxEnumValues { return &SchemaComplexityError{ Dimension: "enum_values", Actual: maxEnum, Limit: openAIMaxEnumValues, } } return nil } // schemaMaxDepth 计算 schema 的最大嵌套深度(每个 JSON 对象/数组各算一层). // // 精妙之处(CLEVER): "嵌套深度"对 OpenAI 是指 schema 关键字的嵌套, // 而非 JSON 对象本身--properties 中的子对象才算一层, // 纯 string/number 等叶子节点不算深度增加. // 我们保守计算:每层 JSON 对象/数组各算 +1,与 OpenAI 实际行为一致(2026-04 probe). func schemaMaxDepth(raw json.RawMessage, current int) int { if len(raw) == 0 { return current } switch raw[0] { case '{': var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { return current } max := current + 1 for _, v := range obj { if d := schemaMaxDepth(v, current+1); d > max { max = d } } return max case '[': var arr []json.RawMessage if err := json.Unmarshal(raw, &arr); err != nil { return current } max := current + 1 for _, elem := range arr { if d := schemaMaxDepth(elem, current+1); d > max { max = d } } return max default: return current } } // schemaTotalProperties 递归统计 schema 中所有层级的 properties 字段数量之和. func schemaTotalProperties(raw json.RawMessage) int { if len(raw) == 0 { return 0 } switch raw[0] { case '{': var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { return 0 } total := 0 if propsRaw, ok := obj["properties"]; ok { var props map[string]json.RawMessage if err := json.Unmarshal(propsRaw, &props); err == nil { total += len(props) for _, v := range props { total += schemaTotalProperties(v) } } } // 递归处理非 properties 字段中的子 schema(items,additionalProperties 等) for k, v := range obj { if k == "properties" { continue // 已处理 } total += schemaTotalProperties(v) } return total case '[': var arr []json.RawMessage if err := json.Unmarshal(raw, &arr); err != nil { return 0 } total := 0 for _, elem := range arr { total += schemaTotalProperties(elem) } return total default: return 0 } } // schemaMaxEnumLen 返回 schema 中所有 enum 数组的最大长度(0 = 无 enum). func schemaMaxEnumLen(raw json.RawMessage) int { if len(raw) == 0 { return 0 } switch raw[0] { case '{': var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { return 0 } maxLen := 0 if enumRaw, ok := obj["enum"]; ok { var arr []json.RawMessage if err := json.Unmarshal(enumRaw, &arr); err == nil { if len(arr) > maxLen { maxLen = len(arr) } } } for k, v := range obj { if k == "enum" { continue } if n := schemaMaxEnumLen(v); n > maxLen { maxLen = n } } return maxLen case '[': var arr []json.RawMessage if err := json.Unmarshal(raw, &arr); err != nil { return 0 } maxLen := 0 for _, elem := range arr { if n := schemaMaxEnumLen(elem); n > maxLen { maxLen = n } } return maxLen default: return 0 } } // derefAnyValue 对任意 json.RawMessage(对象/数组/标量)递归展开 $ref. func derefAnyValue(raw json.RawMessage, defs map[string]json.RawMessage, visiting map[string]bool) (json.RawMessage, error) { if len(raw) == 0 { return raw, nil } switch raw[0] { case '{': // JSON 对象:递归处理 var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { return raw, nil // 解析失败则原样返回 } expanded, err := derefValue(obj, defs, visiting) if err != nil { return nil, err } out, err := json.Marshal(expanded) if err != nil { return nil, err } return out, nil case '[': // JSON 数组:递归处理每个元素 var arr []json.RawMessage if err := json.Unmarshal(raw, &arr); err != nil { return raw, nil } result := make([]json.RawMessage, len(arr)) for i, elem := range arr { expanded, err := derefAnyValue(elem, defs, visiting) if err != nil { return nil, fmt.Errorf("index %d: %w", i, err) } result[i] = expanded } out, err := json.Marshal(result) if err != nil { return nil, err } return out, nil default: // 标量(string/number/bool/null):原样返回 return raw, nil } }