package pricing import ( "git.flytoex.net/yuanwei/flyto-agent/pkg/config" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // LoadAndRegister 从默认路径加载 capabilities 并注册到 registry. // 返回 (注册数量, 错误). // // 精妙之处(CLEVER): 文件不存在不是错误--消费者可能还没跑过 probe, // 此时降级为空 registry(cost 显示为 0 但不阻塞启动). // 这个契约把"文件缺失"和"数据损坏"区分开:前者是正常状态,后者才需要告警. func LoadAndRegister(registry *config.ModelRegistry) (int, error) { return LoadAndRegisterFrom(registry, "") } // LoadAndRegisterFrom 从指定路径加载并注册. // 空路径走 DefaultCapabilitiesPath. func LoadAndRegisterFrom(registry *config.ModelRegistry, path string) (int, error) { report, err := Load(path) if err != nil { return 0, err } if report == nil { return 0, nil // 文件不存在 } return RegisterFromReport(registry, report), nil } // RegisterFromReport 将 report 中的模型信息注册到 registry. // 返回成功注册的模型数量. // // 精妙之处(CLEVER): 非破坏式注册-- // core 的 ModelRegistry.Register 会覆盖同名模型,但我们不清空 registry, // 调用方可以先 Register 一批手工配置的模型,再调 LoadAndRegister 用 probe 数据增量覆盖. // 这样允许"probe 数据 + 手工覆盖"的混合策略. func RegisterFromReport(registry *config.ModelRegistry, report *CapabilityReport) int { if report == nil || registry == nil { return 0 } count := 0 for _, caps := range report.Models { info := toModelInfo(caps) if info == nil { continue } registry.Register(info.ID, info) count++ } return count } // toModelInfo 将 probe 的 ModelCapabilities 转换为 flyto.ModelInfo. // // 精妙之处(CLEVER): Capability 的 Value 是 any,必须做类型断言. // 数值字段从 JSON 解析出来默认是 float64(Go 的 encoding/json 规范), // 所以 asInt 要处理 float64 → int 的转换;asBool 只接受真正的 bool. // 若某字段缺失或类型不匹配,跳过赋值让零值生效--这样 registry 里的模型至少有 ID, // 引擎调用 EstimateCost 时返回 0 而非崩溃. // // 替代方案:<用 json.Number 保留精度> - 否决:Capability.Value 要兼容 bool/string, // 单独给数值用 json.Number 会让结构体字段类型分叉,复杂度不划算. func toModelInfo(caps *ModelCapabilities) *flyto.ModelInfo { if caps == nil || caps.Model == "" { return nil } info := &flyto.ModelInfo{ ID: caps.Model, Provider: caps.Provider, } if v, ok := asInt(caps.ContextWindow.Value); ok { info.ContextWindow = v } if v, ok := asInt(caps.MaxOutputTokens.Value); ok { info.MaxOutputTokens = v } if v, ok := asFloat(caps.InputPricePer1M.Value); ok { info.InputPricePer1M = v } if v, ok := asFloat(caps.OutputPricePer1M.Value); ok { info.OutputPricePer1M = v } if v, ok := asFloat(caps.CacheReadPricePer1M.Value); ok { info.CacheReadPricePer1M = v } if v, ok := asFloat(caps.CacheWritePricePer1M.Value); ok { info.CacheWritePricePer1M = v } if v, ok := asBool(caps.Thinking.Value); ok { info.SupportsThinking = v } if v, ok := asBool(caps.Caching.Value); ok { info.SupportsCaching = v } if v, ok := asBool(caps.Vision.Value); ok { info.SupportsVision = v } // 升华改进(ELEVATED): data-driven-capabilities RFC PR1.1 - 把 probe 实测的 // max_tools 上限和 exhaustive 标志传到 ModelInfo,让 provider.Stream 优先使用 registry 数据. // 之前 toModelInfo 完全没读 caps.MaxTools,导致 probe 数据写进 capabilities.json // 但运行时永远拿不到,是 RFC §3 描述的"数据-行为分离"漏洞之一. if v, ok := asInt(caps.MaxTools.Value); ok { info.MaxTools = v } if caps.MaxTools.Exhaustive != nil { info.MaxToolsExhaustive = *caps.MaxTools.Exhaustive } // CachingMinTokens 字段已存在于 flyto.ModelInfo(line 168),但 toModelInfo // 之前没读 - 现在补上.probe 当前没专门探测这个阈值(anthropic 文档值), // 留接口让未来 probe 增强后自动接入. // 注意:probe 端 capabilities.json 当前没有 caching_min_tokens 字段, // 这里读 caps.Caching.Evidence["min_tokens"] 是预留通道,nil 时跳过. if caps.Caching.Evidence != nil { if v, ok := asInt(caps.Caching.Evidence["min_tokens"]); ok { info.CachingMinTokens = v } } return info } // asInt 将 any 转为 int.JSON 数字默认是 float64. // // 历史包袱(LEGACY): encoding/json 把所有数字解析成 float64-- // 即使原始 JSON 是 `200000` 整数,Go 里也是 float64(200000). // 所以这个函数必须优先处理 float64,否则所有数值字段都会 fallback 到零值. func asInt(v any) (int, bool) { if v == nil { return 0, false } switch n := v.(type) { case float64: return int(n), true case int: return n, true case int64: return int(n), true } return 0, false } // asFloat 将 any 转为 float64. func asFloat(v any) (float64, bool) { if v == nil { return 0, false } switch n := v.(type) { case float64: return n, true case int: return float64(n), true case int64: return float64(n), true } return 0, false } // asBool 将 any 转为 bool. // 只接受真正的 bool 类型,不接受 "true"/"false" 字符串或 1/0 数字-- // probe 产出的能力 JSON 是严格 schema,不应出现类型漂移. func asBool(v any) (bool, bool) { if v == nil { return false, false } if b, ok := v.(bool); ok { return b, true } return false, false }