package flyto import ( "context" "encoding/json" ) // provider.go - 模型提供商接口(纯工厂模式). // // 设计:纯工厂,最大公约数接口. // // - 引擎只认 ModelProvider 接口,调用 Stream() 取事件流. // - 一切 provider 特有功能(Thinking,Caching,Batch 等) // 在工厂构造时通过 provider 自己的 Config 配置,引擎完全不感知. // - 消费者通过工厂函数创建 provider 并注入引擎: // // provider := someprovider.New(someprovider.Config{ // APIKey: "...", // ThinkingBudget: 8000, // provider 内部处理,引擎不知道 // }) // engine.New(&engine.Config{Provider: provider}) // // 历史包袱(LEGACY): 旧版引擎在 Config 顶层有 APIKey / BaseURL, // 所有消费者直接依赖特定 provider 的端点格式. // 新设计通过 ModelProvider 接口隔离,任何 provider 都能接入. // ModelProvider 是模型提供商的核心接口(最大公约数). // // 已有实现: // - pkg/providers/anthropic - Anthropic 原生 // - pkg/providers/openai - OpenAI 原生 // - pkg/providers/openrouter - OpenRouter 聚合(OpenAI 兼容) // - pkg/providers/minimax - MiniMax 原生 // - pkg/providers/gemini - Google Gemini 原生(AI Studio + Vertex AI 双模式)[待 probe 验证] // - pkg/providers/ollama - Ollama 本地部署(OpenAI 兼容) // - pkg/providers/lmstudio - LM Studio 本地部署(OpenAI 兼容) // // Shape: push (stream) from engine's perspective. Engine calls // Stream(ctx, req) and ranges the returned channel; the provider fans out // upstream API chunks as flyto.Event sequence. Consumers wanting to plug // a new model backend implement this interface. // // 形态: 引擎视角 push (流). 引擎调 Stream(ctx, req), range 返回的 channel; // provider 将上游 API chunks 分发为 flyto.Event 序列. 要接新模型后端的 // 消费者实现此接口. type ModelProvider interface { // Name 返回 provider 标识,如 "openai","ollama". Name() string // Stream 向模型发送请求,返回事件流. // // 精妙之处(CLEVER): channel 而非回调-- // channel 天然背压(consumer 慢则 provider 等待), // 与 Go 的 for-range 习惯完美契合,支持 context 取消. // // provider 负责将 API 响应转换为 flyto.Event 事件序列. // channel 关闭表示流结束,最后一个事件为 *ErrorEvent 表示出错. Stream(ctx context.Context, req *Request) (<-chan Event, error) // Models 返回此 provider 可用的模型列表. // 用于模型选择 UI,合法性验证,定价展示,token 进度条等. Models(ctx context.Context) ([]ModelInfo, error) } // ResponseFormat 控制模型的输出格式约束(跨 provider 最大公约数). // // 精妙之处(CLEVER): 只放真正跨 provider 通用的格式-- // "json_object" 几乎所有现代 provider 都原生支持(OpenAI/OpenRouter/MiniMax/Gemini). // 部分 provider 的结构化输出需要特殊 header + json_schema,属于 provider 特有能力, // 通过 provider Config 配置,不出现在此通用类型中. // 替代方案:<把 json_schema + 特殊 flag 都塞进来> - 否决: // 绑定了特定 provider 实现细节,其他 provider 无法使用这些字段, // 造成"通用"接口实际上只有一个 provider 能完整支持. type ResponseFormat struct { // Type 是格式类型: // "json_object" - 约束输出为合法 JSON 对象(所有主流 provider 支持) // "json_schema" - 按指定 schema 约束(OpenAI/OpenRouter/MiniMax 原生支持; // 不支持的 provider 降级为 fence strip,不传 schema) Type string // JSONSchema 是 json_schema 类型的 schema 定义(json_schema 时必填,其他类型忽略) JSONSchema json.RawMessage `json:"json_schema,omitempty"` } // Request 是发给 ModelProvider 的请求(最大公约数字段). // // 升华改进(ELEVATED): 只包含所有 provider 通用的字段. // provider 特有参数(Thinking budget,cache breakpoints,beta flags 等) // 在 provider 工厂的 Config 里配置,不污染公共 Request 类型. // // Auto-caching 行为:当 system prompt 超过模型阈值时,引擎自动注入 cache_control, // 消费者无需声明.阈值来自 ModelInfo.CachingMinTokens(官方文档 + probe 实测). type Request struct { Model string // 模型 ID,如 "claude-sonnet-4-6" Messages []Message // 对话历史(含工具调用/结果) System string // 系统提示(空 = 不使用) MaxTokens int // 最大输出 token 数 Tools []Tool // 可用工具列表(空 = 不使用工具) // ResponseFormat 约束输出格式(nil = 文本,不限制). // 仅 "json_object" 类型在所有主流 provider 中通用. // Provider 特有的 json_schema + 额外 header 通过 provider Config 配置. ResponseFormat *ResponseFormat // NeedsThinking: 消费者声明需要扩展思考(thinking 是有感知的,消费者主动声明). // 引擎无法判断业务复杂度,因此 thinking 必须由消费者显式开启,而非自动决策. // Provider 自动注入 ThinkingBudget 或 reasoning 参数. // 若模型不支持 thinking,silently skip(不报错). // 对比 Config 级别的 ThinkingBudget:Config 是全局开关,NeedsThinking 是 per-request 开关. NeedsThinking bool // FastMode 启用快速模式(影响 max_tokens 默认值 + provider 专有 beta header). // 仅部分 provider 支持,不支持的 provider 应忽略此字段. FastMode bool // Effort 努力级别("low"/"medium"/"high"),空字符串表示不设置. // 仅部分 provider 支持,不支持的 provider 应忽略此字段. Effort string // Temperature controls per-request sampling temperature. Nil = use the // provider/model default (do not transmit a temperature field on the // wire). Non-nil = use this value; each provider passes it through to // its native API and lets the upstream service validate (Anthropic // 0-1, OpenAI/Gemini/Ollama 0-2, MiniMax (0,1], OpenRouter per-model, // LMStudio backend-defined). Out-of-range values surface as the // provider's natural 4xx ErrorEvent rather than client-side clamp -- // this matches industry consensus (Vercel AI SDK / LangChain / // instructor passthrough; only litellm tries drop_params and is // empirically buggy). One in-Request deterministic conflict gets // pre-handled: Anthropic + NeedsThinking + Temperature != 1.0 → // silent override to 1.0 + WarningEvent (server otherwise 400s). // // Temperature 控制本次请求的采样温度. nil = 用 provider/模型默认 // (wire 上不传 temperature 字段); 非 nil = 用该值, 每个 provider 直接 // 透传到原生 API 由上游服务校验 (Anthropic 0-1, OpenAI/Gemini/Ollama // 0-2, MiniMax (0,1], OpenRouter 按 model 各异, LMStudio 由 backend // 决定). 越界一律不在 flyto 层 clamp, 由上游 4xx 自然冒泡为 // ErrorEvent -- 与业界共识一致 (Vercel AI SDK / LangChain / // instructor 全 passthrough; 只有 litellm 尝试 drop_params 且 bug // 频发). 仅一个 wire 时已知冲突预拦: Anthropic + NeedsThinking + // Temperature != 1.0 → silent override 1.0 + WarningEvent (否则 // 服务端 400). // // CLEVER: nullable *float64 而非 float64 + sentinel -- Go 零值 0 // 在采样语义里是合法的 deterministic, 不能复用作 "未设". helper // flyto.Float() 简化指针字段构造. Temperature *float64 // TopP controls nucleus sampling cutoff (1.0 = disabled). Nil = use // the provider/model default. Non-nil = use this value; same // passthrough policy as Temperature -- upstream validates, out-of-range // surfaces as 4xx ErrorEvent. One pre-handled conflict: Anthropic + // NeedsThinking restricts top_p to [0.95, 1.0]; values below 0.95 // silent-override to 1.0 + WarningEvent. // // TopP 控制 nucleus 采样阈值 (1.0 = 不启用). nil = 用 provider/ // 模型默认; 非 nil = 用该值. 与 Temperature 同 passthrough 策略, // 上游校验, 越界 4xx 自然冒泡. 仅一个预拦特例: Anthropic + // NeedsThinking 限制 top_p 在 [0.95, 1.0], 低于 0.95 silent override // 1.0 + WarningEvent. TopP *float64 // SystemBlocks 分段系统提示词(支持 per-block 缓存策略). // 如果非 nil 且非空,Provider 应优先使用此字段而非 System string. // 每个 block 有 Text 和 CacheScope 字段. SystemBlocks []SystemBlock // Capabilities 是本次请求关联的模型能力快照(由 engine 在调用 provider 之前注入). // // 升华改进(ELEVATED): data-driven-capabilities RFC 的核心机制-- // engine 每次调用 provider.Stream 之前从 ModelRegistry 取出当前模型的 ModelInfo, // 塞进这个字段.Provider 优先读取这里的字段(registry 数据驱动), // 缺失时降级到 provider 包内的硬编码常量(向后兼容兜底). // // 设计要点: // - nil = engine 未注入(单元测试 / mock 场景 / model 不在 registry 中), // provider 必须降级到包内常量,行为完全等同于现状(零回归) // - 非 nil = engine 已注入快照,provider 优先使用此字段 // - 这是只读快照--provider 不应修改它的字段,registry 后续修改也不影响 // 进行中的请求(值的指针,但 ModelInfo 字段都是值类型) // // 替代方案: - 否决: // 会让 provider 包 import config 包,引入依赖循环风险; // 而 Request 字段注入让 provider 完全无感知 registry 的存在. // 替代方案:<让 engine 全部预先解析完成后只传 maxTools/supportsThinking 等扁平字段> - 否决: // flyto.Request 字段会膨胀,且每加一个能力都要改 Request schema. Capabilities *ModelInfo } // Float returns a pointer to v. Sugar for setting *float64 fields like // Request.Temperature / Request.TopP without a temporary local variable. // // Float 返回指向 v 的指针. 给 Request.Temperature / Request.TopP 这类 // *float64 字段赋值时省去临时局部变量. func Float(v float64) *float64 { return &v } // SystemBlock 是分段系统提示词的一块. type SystemBlock struct { Text string // 文本内容 CacheScope string // "", "session", "global" - 非空表示需要缓存 } // ModelInfo 描述一个模型的规格和能力. // // 用途:token 进度条(ContextWindow),费用估算(Price), // 模型选择 UI(DisplayName),能力过滤(Supports*). type ModelInfo struct { ID string // API 使用的模型 ID,如 "claude-sonnet-4-6" DisplayName string // 展示名称,如 "Claude Sonnet 4.6" Provider string // provider 标识 ContextWindow int // 上下文窗口(tokens) MaxOutputTokens int // 最大输出(tokens) // 定价(USD / 1M tokens,0 = 免费或未知) InputPricePer1M float64 OutputPricePer1M float64 CacheReadPricePer1M float64 // 缓存读取价格(美元/百万 token) CacheWritePricePer1M float64 // 缓存写入价格(美元/百万 token) // 能力标志(provider 填写). // 升华改进(ELEVATED): 早期仅作展示元数据,新版同时驱动引擎自动决策-- // SupportsCaching=true + CachingMinTokens > 0 → 引擎自动检查 system 长度并注入 cache_control(已实现,2026-04) // SupportsThinking=true → 消费者可用 Request.NeedsThinking 触发自动 budget 注入(已实现,2026-04) // 替代方案:<把所有能力逻辑放在消费者侧> - 否决:每个消费者都得手动处理阈值/header/格式差异. // // SupportsCaching 已知支持(2026-04): // anthropic claude-opus-4-6 / claude-sonnet-4-6 : 1024t(官方文档) // anthropic claude-haiku-4-5 : 4096t(官方文档) // minimax MiniMax-M2.7/M2.5/M2.1/M2 : 1024t(probe 实测) // openrouter → anthropic 路径 : ✗(probe 确认 cr=0@7200t) SupportsCaching bool SupportsBatch bool SupportsThinking bool SupportsVision bool // CachingMinTokens 是触发 prompt caching 的最低系统提示 token 数. // 数据来源:官方文档 + 实测探测(2026-04). // 0 = 不支持或未知. // // 已知阈值(2026-04): // anthropic claude-opus-4-6 / claude-sonnet-4-6 : 1024t(官方文档) // anthropic claude-haiku-4-5 : 4096t(官方文档;probe 确认需要 ~7200t 才触发,文档值 4096 在范围内) // minimax MiniMax-M2.x : 1024t(probe 实测 cr=1110 @~1000t 系统提示) // // 历史包袱(LEGACY): OpenRouter 路径 cache_control 不被转发给上游后端 // (probe 确认 cr=0@7200t),OpenRouter 路由的模型应设 SupportsCaching=false. CachingMinTokens int // Tool Use 限制(已知,2026-04): // anthropic : strict 模式 ≤ 20 个工具;非 strict 无明确上限(官方文档) // Schema 不支持:数值约束(min/max/multipleOf),minLength/maxLength, // 递归 schema,外部 $ref;minItems 仅 0 或 1 // openai : Chat API ≤ 128 个工具;strict 不支持 allOf/not/if-then-else; // 嵌套 ≤ 10 层;属性总数 ≤ 5000(OpenAPI spec 来源) // minimax : 未记录(待 probe 验证) // openrouter: 取决于底层模型,自身透传无额外限制 // MaxTools 是单次请求允许的最大工具数(0 = 未知/无限制). // // 已知值(2026-04): // OpenAI Chat API : 128(OpenAPI spec 明确记录) // Anthropic strict : 20(官方文档;普通模式无明确文档上限,此处填 0) // MiniMax : 0(probe 实测 @256 未发现上限,待后续验证) // OpenRouter : 0(透传底层模型,自身无额外限制) // // 精妙之处(CLEVER): 用 0 表示"未知/不限制"而非 MaxInt-- // 0 是 Go 零值,静态表不填此字段即表示"不限制",无需显式声明; // MaxInt 在各层传递时容易溢出或被误用为"极大数"参与比较运算. MaxTools int // MaxToolsExhaustive 表示 MaxTools 是否是穷尽测试得到的真上限. // // 升华改进(ELEVATED): 与 cmd/capability-probe 输出的 max_tools.exhaustive // schema 对应.区分两种语义: // true = MaxTools 是确认上限,provider 可硬执行(len(tools) > MaxTools 即报错) // false = MaxTools 是已知下界(probe 测到这个数没出错,但没找到真上限), // provider 应软处理(继续发请求让 API 自己拒),不应客户端硬拒 // // 默认零值 false 是保守选择:静态表不显式填写时按"已知下界"对待,避免误伤. // 替代方案:<*bool 三态> - 否决:MaxTools=0 已是哨兵语义(无上限), // Exhaustive 仅在 MaxTools > 0 时有意义,bool 零值刚好对应"保守软处理". MaxToolsExhaustive bool }