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 // ThinkingMode is an explicit TRI-STATE thinking switch (nil = omit on the // wire / use provider+model default; "enabled" / "disabled" = force the // thinking channel on/off). It is strictly more expressive than the legacy // NeedsThinking bool, which can only REQUEST thinking (true) but cannot // DISABLE it -- and that gap matters for backends that think BY DEFAULT. // DeepSeek V4 (api.deepseek.com OpenAI-compat) reasons by default; to turn // it OFF you must send `{"thinking":{"type":"disabled"}}` explicitly, which // NeedsThinking=false cannot express (false just omits the field, leaving // the model's default ON). Why this is load-bearing: when thinking is ON, // DeepSeek IGNORES temperature/top_p/presence_penalty/frequency_penalty // (per /guides/thinking_mode), so a sub-agent relying on sampling as its // only anti-loop lever has that lever silently no-op'd unless thinking is // explicitly disabled. Mapping status: only the DeepSeek provider // (ModeOpenAI) maps this today -> top-level `thinking:{type}`. When both // ThinkingMode and NeedsThinking are set, ThinkingMode WINS. Other // providers ignore it (documented no-op, NOT an error); they keep reading // NeedsThinking. // // ThinkingMode 是显式**三态**思考开关 (nil = wire 不传 / 用 provider+模型默认; // "enabled" / "disabled" = 强制开/关思考通道). 比旧的 NeedsThinking bool 严格更 // 有表达力 -- 后者只能**请求**思考 (true) 不能**关闭** (false 只是省略字段, 留 // 模型默认). 对"默认就思考"的后端这个 gap 是致命的: DeepSeek V4 (api.deepseek.com // OpenAI 兼容) 默认推理, 要关必须显式发 `{"thinking":{"type":"disabled"}}`, // NeedsThinking=false 表达不出. 为何 load-bearing: 思考开时 DeepSeek **忽略** // temperature/top_p/presence_penalty/frequency_penalty (官方 /guides/thinking_mode), // 所以靠采样作唯一抗循环杠杆的 sub-agent, 不显式关思考其杠杆就被静默 no-op. // 映射状态: 当前只有 DeepSeek provider (ModeOpenAI) 映射 -> 顶级 `thinking:{type}`. // ThinkingMode 与 NeedsThinking 同设时 ThinkingMode **胜**. 其他 provider 忽略它 // (documented no-op, **非** error), 继续读 NeedsThinking. ThinkingMode *string // FastMode 启用快速模式(影响 max_tokens 默认值 + provider 专有 beta header). // 仅部分 provider 支持,不支持的 provider 应忽略此字段. FastMode bool // Effort 努力级别, 空字符串表示不设置. 仅部分 provider 支持, 不支持者忽略. // 值的取值范围 per-provider: OpenAI o 系列 / OpenRouter = "low"/"medium"/"high"; // DeepSeek V4 thinking_mode = "high"/"max" (reasoning_effort, 默认 high, 复杂 // agent 任务自动升 max). DeepSeek provider (ModeOpenAI) 把它映射成顶级 // reasoning_effort; 仅在 thinking 开启时生效. // // Effort is the reasoning-effort level, empty = unset. Provider-specific // value range: OpenAI o-series / OpenRouter = low/medium/high; DeepSeek V4 // thinking_mode = high/max (reasoning_effort; default high, auto-max for // complex agent tasks). The DeepSeek provider (ModeOpenAI) maps it to a // top-level reasoning_effort; honored only when thinking is enabled. 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 // TopK limits sampling to the K highest-probability tokens. Nil = omit // on the wire (provider/model default). NON-STANDARD across providers -- // the field is uniform on this contract but the MAPPING is per-provider. // Mapping status as of this commit (set TopK only for a backend below // that maps it; elsewhere it is a documented silent no-op, NOT an error): // - openai-compat wire (local vLLM / oMLX, MiniMax, OpenRouter, Ollama, // LMStudio, DeepSeek OpenAI-mode): MAPPED -> top_k. // - Anthropic: native API HAS top_k but NOT YET mapped here (tracked // gap, core/TODO.md -- a TopK on the anthropic provider is dropped). // DeepSeek Anthropic-compat ignores it upstream. // - Official OpenAI (api.openai.com): no top_k concept -> the // openai-compat wire omits the nil field; never set it for that host. // - Gemini: NOT YET mapped (tracked gap). // *int (not int) so explicit 0 (vLLM "disabled / unlimited") stays // distinct from unset (nil); 0 is sent, nil is omitted. Same nullable // convention as Temperature. // // TopK 把采样限制在概率最高的 K 个 token. nil = wire 不传 (provider/模型 // 默认). provider 间**非标准** -- 字段在本契约统一, 但**映射 per-provider**. // 本 commit 映射状态 (只对下面映射了它的 backend 设 TopK; 别处是 documented // 静默 no-op, **非** error): // - openai-compat wire (本地 vLLM / oMLX, MiniMax, OpenRouter, Ollama, // LMStudio, DeepSeek OpenAI 模式): 已映射 -> top_k. // - Anthropic: 原生 API 有 top_k 但此处**暂未映射** (tracked gap, // core/TODO.md -- 给 anthropic provider 设 TopK 会被丢). DeepSeek // Anthropic 兼容上游忽略它. // - 官方 OpenAI (api.openai.com): 无 top_k 概念 -> openai-compat wire // nil 时省略该字段; 别对该 host 设它. // - Gemini: 暂未映射 (tracked gap). // 用 *int (非 int) 让显式 0 (vLLM "关闭 / 不限") 与未设 (nil) 可区分; 0 发, // nil 省略. 与 Temperature 同 nullable 约定. TopK *int // MinP sets a dynamic probability floor: tokens below MinP * P(top token) // are filtered -- a strong anti-repetition / anti-loop lever, often more // effective than top_p. Nil = omit. EVEN MORE provider-specific than // TopK. Mapping status: // - openai-compat wire to a backend implementing it (local vLLM / oMLX, // the ds4 endpoint): MAPPED -> min_p. // - Anthropic / official OpenAI / Gemini: NO min_p concept at all -> // not mapped; setting MinP there is a silent no-op (openai-compat // wire omits nil, the others never map it). // *float64 so explicit 0 ("no floor") stays distinct from unset; 0 is // sent, nil omitted. // // MinP 设动态概率地板: 低于 MinP * P(最高 token) 的 token 被过滤 -- 强抗 // 重复 / 抗循环杠杆, 常比 top_p 更有效. nil = 不传. 比 TopK 更 provider // 特有. 映射状态: // - openai-compat wire 到实现了它的 backend (本地 vLLM / oMLX, ds4 端点): // 已映射 -> min_p. // - Anthropic / 官方 OpenAI / Gemini: 根本没有 min_p 概念 -> 未映射; // 在那设 MinP 是静默 no-op (openai-compat wire nil 时省略, 其余本就 // 不映射). // 用 *float64 让显式 0 ("无地板") 与未设可区分; 0 发, nil 省略. MinP *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 // SupportsTranscription marks an audio speech-to-text model (served via // /v1/audio/transcriptions, flyto.TranscriptionProvider). Filled by live // discovery from the oMLX catalog's model_type=audio_stt; static catalogs // set it per model. A transcription model is NOT a chat model -- routing // engine turns at it fails server-side. // // SupportsTranscription 标记语音转文字模型 (经 /v1/audio/transcriptions, // flyto.TranscriptionProvider 服务). live discovery 从 oMLX 目录的 // model_type=audio_stt 填; 静态目录逐模型设. 转写模型**不是** chat 模型 -- // 引擎轮次路由到它会在服务端失败. SupportsTranscription 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 // ADR-0007 (capability tracking 接入纪律, 2026-05-01): 3 个新增维度 // 让 wire 层能 capability-aware 决定行为而非依赖兜底假设. 零值 = // 未知 = 旧行为零回归 (静态表不显式填写时不影响现有 caller). // // ADR-0007 (capability tracking 接入纪律, 2026-05-01): 3 新维度让 // wire 层 capability-aware 决定行为, 零值=未知=旧行为零回归. // ToolNameRegex is the provider-enforced regex pattern for tool // (function) names. Examples: // - OpenAI / OpenAI-compatible: ^[a-zA-Z0-9_-]+$ // - Anthropic: ^[a-zA-Z0-9_-]{1,64}$ // // Empty = unknown / not enforced; wire layer skips pre-flight // validation and lets provider HTTP 4xx surface naturally (covered // by ADR-0006 typed errors). Non-empty = wire layer pre-flight // validates each tool.Name against the regex; mismatch returns // flyto.EngineError(ErrModelToolUnsupported, Detail: "tool name // X violates regex Y"). // // Driver: r22 实证 OpenRouter→SiliconFlow reject "billcost.reflect" // (含 dot) 的 [20015] HTTP 400. 早期发现胜过事后归类. // // ToolNameRegex 是 provider 强制要求的工具名 regex. 例: // - OpenAI / OpenAI 兼容: ^[a-zA-Z0-9_-]+$ // - Anthropic: ^[a-zA-Z0-9_-]{1,64}$ // 空 = 未知/不强制; wire 层跳过 pre-flight 让 provider HTTP 4xx // 自然冒泡 (ADR-0006 typed errors 路径). 非空 = wire 层 pre-flight // 校验, 命中 mismatch 返 ErrModelToolUnsupported. // // 驱动: r22 实证 OpenRouter→SiliconFlow 拒 "billcost.reflect" 的 // HTTP 400. 早期发现胜事后归类. ToolNameRegex string // ReasoningPassbackMode declares whether and how the model expects // prior assistant turn's thinking content to be passed back in the // next request's assistant message. Values: // - "" (zero) = unknown; wire layer skips passback (current // pre-ADR-0007 behavior, zero-regression default) // - "none" = model server manages reasoning state // (OpenAI o1/o3); client must NOT pass back reasoning_content // - "string" = client passes prior thinking back as // assistant message reasoning_content string field (DeepSeek-R1 // / SiliconFlow protocol) // - "details_array" = client passes prior thinking back as // assistant message reasoning_details array (some OpenRouter // paths -- TBD when first encountered) // // Driver: r24 实证 deepseek-v4-flash HTTP 400 "The reasoning_content // in the thinking mode must be passed back to the API". 加 "string" // 模式让 wire 层 inject prior turn 的 BlockThinking 到 reasoning_content. // // ReasoningPassbackMode 声明模型是否要求 prior assistant turn 的 // thinking 在下一轮请求里回传以及形态. 值: // - "" (零值) = 未知, wire 跳过 passback (ADR-0007 前行为零回归) // - "none" = 模型 server 管 state (OpenAI o1/o3), // client 必须**不**传 reasoning_content // - "string" = client 传 reasoning_content 字符串字段 // (DeepSeek-R1 / SiliconFlow 协议) // - "details_array" = client 传 reasoning_details 数组 // (部分 OpenRouter 路径, TBD 首遇时) // // 驱动: r24 实证 deepseek-v4-flash HTTP 400 "reasoning_content must // be passed back". "string" 模式让 wire 层 inject prior turn 的 // BlockThinking 到 reasoning_content. ReasoningPassbackMode string // ProviderKind classifies the provider for ADR-0007 strict-mode // bifurcation. Values: // - "" (zero) = unknown; treated as "aggregator" (conservative, // skip strict refuse, zero-regression) // - "direct" = anthropic / openai / minimax / gemini etc. -- // fixed (provider × model) set; strict refuse on capabilities_ // missing is feasible // - "aggregator" = openrouter / lmstudio / ollama -- model_id is // dynamically routed; cannot probe all in advance; must fall // back to wire-level capability inference (live API metadata) // and provider 4xx surface // // 业界对照: LiteLLM / Aider / Vercel AI SDK / LangChain 全把 // aggregator 路径走 passthrough + provider 4xx; 没有主流框架对 // aggregator 做 strict refuse. ADR-0007 § 2.2 保留这条 bifurcation // 不试图改业界共识. // // ProviderKind 给 ADR-0007 strict 模式分流标签. 值: // - "" (零值) = 未知, 视作 "aggregator" 保守 (跳过 strict refuse, // 零回归) // - "direct" = anthropic / openai / minimax / gemini 等固定 // (provider × model) 集; capability 缺失时 strict refuse 可行 // - "aggregator" = openrouter / lmstudio / ollama 等模型动态路由; // 不可能预先 probe 全; 必须走 wire 层 capability 推断 (live API // metadata) + provider 4xx 自然冒泡 // // 业界对照: LiteLLM / Aider / Vercel AI SDK / LangChain 全 aggregator // passthrough + provider 4xx, 主流框架无 aggregator strict refuse. // ADR-0007 § 2.2 bifurcation 保留不挑战业界共识. ProviderKind string }