// Package pricing 从 probe 产出的 capabilities.json 加载模型定价信息. // // 升华改进(ELEVATED): 原架构把模型定价硬编码在 core 的 DefaultModels map 里-- // 清空后引擎不知道任何模型的价格,cost 永远显示 0. // pricing 模块让 probe 产出的静态 JSON 文件成为 runtime 数据源: // // probe 跑一次 → 写 ~/.flyto/capabilities/capabilities.json // 消费者启动时 → pricing.LoadAndRegister(registry) // 引擎自动拥有正确的定价信息,cost 显示正确. // // 替代方案:<让消费者手动 Register 每个模型> - 否决:繁琐且容易漏模型. // // 跨场景复用(ELEVATED): // - TUI: 启动时调用 LoadAndRegister,让状态栏显示正确 cost. // - platform server: 每个请求前从 registry 查模型定价做计费. // - CI 工具: 通过 FLYTO_CAPABILITIES_PATH 注入测试 fixture,跑离线成本分析. package pricing import ( "encoding/json" "fmt" "os" "path/filepath" ) // Capability 是单项能力的结构(对应 probe 输出的格式). // // 精妙之处(CLEVER): Value 用 any 而非 float64/bool/int 具体类型-- // probe 的每项能力值可能是数字,布尔,字符串,统一成 any 保留原始类型, // 消费者通过类型断言(asInt/asFloat/asBool)按需转换. type Capability struct { Value any `json:"value"` Source string `json:"source"` // Exhaustive 表示 Value 是否是穷尽测试得到的真上限. // 升华改进(ELEVATED): 与 cmd/capability-probe 的同名字段 schema 一致,参见那边的详细说明. // 简言之:nil = 概念不适用 / 老 schema;false = Value 是已知下界;true = Value 是确认上限. // 下游消费者(engine 的 wire.CheckToolCount 等)应区分这三种状态: // - true 时可硬执行 "len(tools) > Value 即报错"; // - false 时只能 "len(tools) > Value 时降级警告",不能直接拒绝; // - nil 时不执行任何判断,降级到 provider 包内的硬编码常量. Exhaustive *bool `json:"exhaustive,omitempty"` Evidence map[string]any `json:"evidence,omitempty"` Note string `json:"note,omitempty"` } // ModelCapabilities 是单个模型的完整能力集(对应 probe 的输出格式). // // 升华改进(ELEVATED): 原本考虑把字段拆成强类型的 ContextWindowInt/StreamingBool 等, // 但反向思考后决定保留 Capability 包装类型-- // probe 的 source/evidence/note 元数据对审计和调试至关重要, // 如果丢掉这些信息,后续排查"为什么这个模型的 context window 是 200k"时就没线索了. // 替代方案:<直接用 ContextWindow int> - 否决:丢失 source 和 evidence 元信息. type ModelCapabilities struct { Provider string `json:"provider"` Model string `json:"model"` ContextWindow Capability `json:"context_window"` MaxOutputTokens Capability `json:"max_output_tokens"` InputPricePer1M Capability `json:"input_price_per_1m"` OutputPricePer1M Capability `json:"output_price_per_1m"` CacheReadPricePer1M Capability `json:"cache_read_price_per_1m,omitempty"` CacheWritePricePer1M Capability `json:"cache_write_price_per_1m,omitempty"` Streaming Capability `json:"streaming,omitempty"` Thinking Capability `json:"thinking,omitempty"` ToolUse Capability `json:"tool_use,omitempty"` Caching Capability `json:"caching,omitempty"` Vision Capability `json:"vision,omitempty"` // MaxTools 是该模型实测的工具数量上限. // 升华改进(ELEVATED): 此前 loader 不读这个字段(probe 写但无人消费),导致 capability-probe // 的探测成果脱离运行时--下游 wire.CheckToolCount 仍用各 provider 包内的硬编码常量. // 现在加入 schema 是为了配合后续 RFC(data-driven-capabilities-rfc): // engine/provider 的 Stream() 路径将优先查 registry 中的 MaxTools.Value,缺失时降级到 const. // Exhaustive 字段决定可不可以硬执行:true 时硬拒绝,false 时仅警告. MaxTools Capability `json:"max_tools,omitempty"` } // CapabilityReport 是完整报告(对应 probe 产出的顶层 JSON). type CapabilityReport struct { SchemaVersion string `json:"schema_version"` GeneratedAt string `json:"generated_at"` Models map[string]*ModelCapabilities `json:"models"` } // DefaultCapabilitiesPath 返回默认的 capabilities.json 路径. // 优先级:$FLYTO_CAPABILITIES_PATH > ~/.flyto/capabilities/capabilities.json. // // 精妙之处(CLEVER): 环境变量优先级最高--CI 和单元测试可以注入 fixture 文件, // 不污染用户的 ~/.flyto/ 目录,也不需要 mock 文件系统. func DefaultCapabilitiesPath() string { if p := os.Getenv("FLYTO_CAPABILITIES_PATH"); p != "" { return p } home, err := os.UserHomeDir() if err != nil { return "" } return filepath.Join(home, ".flyto", "capabilities", "capabilities.json") } // Load 从指定路径加载 capabilities 报告. // 路径为空时使用 DefaultCapabilitiesPath(). // // 精妙之处(CLEVER): 文件不存在返回 (nil, nil) 而非 error-- // 消费者启动时可能还没跑过 probe,这是正常降级状态, // 调用方检查 report == nil 决定用空 registry 继续启动还是报错退出. // 只有真正的 I/O 错误或 JSON 解析错误才是 error. func Load(path string) (*CapabilityReport, error) { if path == "" { path = DefaultCapabilitiesPath() } if path == "" { return nil, fmt.Errorf("pricing: no path available (home dir unresolvable and FLYTO_CAPABILITIES_PATH unset)") } data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, nil // 文件不存在,调用方降级 } return nil, fmt.Errorf("pricing: read %s: %w", path, err) } var report CapabilityReport if err := json.Unmarshal(data, &report); err != nil { return nil, fmt.Errorf("pricing: parse %s: %w", path, err) } return &report, nil }