// Package minimax 实现 MiniMax 官方 API 的 ModelProvider. // // MiniMax 提供三种 API 接入方式: // 1. Native API - /v1/text/chatcompletion_v2(官方路径,本实现默认) // 2. OpenAI 兼容 - /v1/chat/completions // 3. Anthropic 兼容 - /anthropic/v1/messages(MiniMax 对外推广,但实测效果不稳定) // // 默认使用 Native 模式,原因: // - Native 是 MiniMax 的第一方 API,语义最完整 // - 实测 Anthropic 兼容端点在编程类 Agent 场景下表现较差 // (可能是 Anthropic 特有的 beta headers / system prompt 格式未完全兼容) // - OpenAI 兼容和 Native 的 SSE 格式相同,切换成本低 // // Native API 的 SSE 格式与 OpenAI 完全相同(data: {...}, data: [DONE]), // 因此我们复用 internal/wire/openai_compat.go,只需修改端点路径. // // 历史包袱(LEGACY): flysafe 早期通过 BaseURL="https://api.minimaxi.com/anthropic" // 使用 MiniMax,走的是 Anthropic 兼容端点. // 迁移路径:flysafe 改用 minimax.New(Config{APIKey: "..."}) 即可, // 无需关心底层使用哪种格式. package minimax import ( "context" "fmt" "net/http" "time" "git.flytoex.net/yuanwei/flyto-agent/internal/transport" "git.flytoex.net/yuanwei/flyto-agent/internal/transport/retry" "git.flytoex.net/yuanwei/flyto-agent/internal/wire" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" "git.flytoex.net/yuanwei/flyto-agent/pkg/providers/shared" ) // Mode 是 MiniMax API 接入模式. type Mode string const ( // ModeNative 使用 MiniMax 官方 API(/v1/text/chatcompletion_v2). // 推荐:语义最完整,SSE 格式与 OpenAI 相同,思考内容通过 delta.reasoning 字段传递. ModeNative Mode = "native" // ModeOpenAI 使用 MiniMax 的 OpenAI 兼容端点(/v1/chat/completions). // 与 ModeNative 几乎等价,端点路径不同. ModeOpenAI Mode = "openai" // ModeAnthropic 使用 MiniMax 的 Anthropic 兼容端点(/anthropic/v1/messages). // 注意:实测在编程类 Agent 场景下效果可能不如 Native. // 仅在明确需要 Anthropic 格式特性(如 cache_control 细粒度控制)时使用. ModeAnthropic Mode = "anthropic" ) // Region 是 MiniMax API 地区. type Region string const ( // RegionGlobal 使用国际节点(api.minimax.io). RegionGlobal Region = "global" // RegionChina 使用中国节点(api.minimaxi.com). RegionChina Region = "china" ) // Config 是 MiniMax provider 的配置. type Config struct { // APIKey 是 MiniMax API 密钥(从 platform.minimax.io 获取). APIKey string // Region 控制使用国际节点还是中国节点(默认 RegionChina). // MiniMax 主要面向中国市场,默认使用国内节点(api.minimaxi.com). // 如需使用国际节点(api.minimax.io),显式设置 RegionGlobal. Region Region // Mode 控制使用哪种 API 格式(默认 ModeNative). Mode Mode // ThinkingBudget 启用思考模式(仅 M2.1+ 支持). // 0 = 禁用.推荐值:5000-16000. // ModeNative/ModeOpenAI 时通过 reasoning_split 参数传递. // ModeAnthropic 时通过 thinking.budget_tokens 传递. ThinkingBudget int // HTTPClient 注入自定义 HTTP 客户端. // nil = 使用默认 http.Client (带 Timeout 字段配置的 ResponseHeaderTimeout). // 非 nil 时 consumer 完全接管超时责任, 下面的 Timeout 字段被忽略. // // 精妙之处(CLEVER): minimax 的 HTTPClient 会被双模式 (ModeAnthropic / ModeNative|OpenAI) // 两个独立客户端同时使用 - 消费者注入的 client 会被两边共享.这是 MiniMax 特有的, // 因为同一个 Provider 实例只会选一种 Mode, 不会同时用两个 client. HTTPClient *http.Client // Timeout 限制"从请求发出到收到响应首字节"的时间. // // 通过 http.Transport.ResponseHeaderTimeout 实现, **不影响** SSE 流式响应的后续 // body 读取 - MiniMax 长思考模式 (M2.7 Thinking) 可以正常读完. // // 精妙之处(CLEVER): 不要误用 http.Client.Timeout - 那会把 SSE 流砍死. // MiniMax 的双模式 (ModeAnthropic / ModeNative|OpenAI) 都从这一个字段读超时, // 两个底层 client 独立应用, 语义保持等价. // // 0 = 使用 defaultTimeout (60s, 适合 MiniMax 云端). // 仅当 HTTPClient 为 nil 时生效; 提供自定义 HTTPClient 时此字段被忽略. Timeout time.Duration // ModelOverrides 覆盖静态模型表. ModelOverrides []flyto.ModelInfo } // defaultTimeout 是 Config.Timeout == 0 且 Config.HTTPClient == nil 时的 ResponseHeaderTimeout 兜底值. // 60s 对 MiniMax 云端 (国内 api.minimaxi.com / 国际 api.minimax.io) 合理. // M2.7 Thinking 模式可能在开始响应前思考数秒, 但 60s 仍充足. const defaultTimeout = 60 * time.Second // GoString 实现 fmt.GoStringer,防止 %#v 打印时泄露 APIKey. // 升华改进(ELEVATED): 参见 anthropic.Config.GoString 的说明,同理. func (c Config) GoString() string { return shared.GoStringWithMaskedKey("Config", c.APIKey) } // Provider 是 MiniMax ModelProvider 实现. type Provider struct { cfg Config baseURL string wireClient *wire.OpenAICompatClient // Native/OpenAI 模式 anthroClient *api.Client // Anthropic 兼容模式 } // New 创建 MiniMax ModelProvider. func New(cfg Config) *Provider { if cfg.Region == "" { cfg.Region = RegionChina } if cfg.Mode == "" { cfg.Mode = ModeNative } var baseURL string switch cfg.Region { case RegionChina: baseURL = "https://api.minimaxi.com" default: baseURL = "https://api.minimax.io" } p := &Provider{cfg: cfg, baseURL: baseURL} switch cfg.Mode { case ModeAnthropic: // Anthropic 兼容模式:复用 internal/transport(package api),BearerAuth=true. // // 升华改进(ELEVATED): 2026-04-11 - 补齐与 anthropic.New() 构造链对等的 4 个 // robustness option.此前 ModeAnthropic 只注入 WithMessagePath + WithBearerAuth, // 缺少 WithAPIVersion / WithClassifier / WithRetryPolicy / WithOverflowHandler, // 导致错误分类,429/529 重试,max_tokens 溢出修正器全部缺失. // 成功路径不受影响(curl + Go 端到端 integration test 已双重验证), // 但错误/重试/异常路径的行为不如 anthropic.New() 健壮. // // 替代方案:<保持最小构造,让消费者自己 WithHTTPClient(custom)> - 否决: // 把 Anthropic 协议特有的选项(比如 AnthropicClassifier)推给消费者违反 // "provider 内聚 protocol 细节" 原则.消费者不需要知道 Anthropic 兼容端点的 // retry/overflow 细节. // // 反向思维:WithAPIVersion 要不要?minimax 端实测不要求此 header(不带也返回 200), // 但 anthropic.New() 默认会注入.为了 ModeAnthropic 与 anthropic.New() 两条路径 // 的行为完全对等(便于 flysafe 类消费者从 BaseURL hack 迁移),保留注入. var opts []api.ClientOption opts = append(opts, api.WithMessagePath("/v1/messages"), api.WithAPIVersion("2023-06-01"), api.WithClassifier(&api.AnthropicClassifier{Hinter: &api.DefaultHinter{}}), api.WithRetryPolicy(retry.NewAnthropicRetryPolicy(retry.AnthropicRetryOpts{})), api.WithOverflowHandler(retry.DefaultOverflowHandler()), ) // 超时配置: HTTPClient 和 Timeout 二选一, 不混用 (见 Config.Timeout 注释). if cfg.HTTPClient != nil { opts = append(opts, api.WithHTTPClient(cfg.HTTPClient)) } else { timeout := cfg.Timeout if timeout == 0 { timeout = defaultTimeout } opts = append(opts, api.WithResponseHeaderTimeout(timeout)) } opts = append(opts, api.WithBearerAuth()) p.anthroClient = api.NewClient(cfg.APIKey, baseURL+"/anthropic", opts...) default: // Native / OpenAI 兼容模式:使用 wire.OpenAICompatClient wireOpts := []wire.OpenAICompatOption{} // 超时配置: 与 ModeAnthropic 分支语义对称,只是走 wire 包的 option. if cfg.HTTPClient != nil { wireOpts = append(wireOpts, wire.WithHTTPClient(cfg.HTTPClient)) } else { timeout := cfg.Timeout if timeout == 0 { timeout = defaultTimeout } wireOpts = append(wireOpts, wire.WithResponseHeaderTimeout(timeout)) } if cfg.Mode == ModeNative { // Native API 使用不同的端点路径 wireOpts = append(wireOpts, wire.WithChatPath("/v1/text/chatcompletion_v2")) } p.wireClient = wire.NewOpenAICompatClient(cfg.APIKey, baseURL, wireOpts...) } return p } var _ flyto.ModelProvider = (*Provider)(nil) // Name 返回 provider 标识. func (p *Provider) Name() string { return "minimax" } // Stream 向 MiniMax API 发起流式请求. // // 升华改进(ELEVATED): data-driven-capabilities RFC PR2.6 - Stream dispatcher 层统一处理 // max_tools 检查 + want×can 检测.两条 stream path(streamOpenAI / streamAnthropic)原本 // 各自做一份检查,提到 dispatcher 层 DRY + 行为一致.能力决策是模型级属性,和序列化格式无关. func (p *Provider) Stream(ctx context.Context, req *flyto.Request) (<-chan flyto.Event, error) { // CAP-7: Tool 数量上限检查 - 统一入口,两条路径共用. // 兜底 minimaxMaxTools=0 跳过检查(probe 未发现上限);registry 可注入实测上限. // 仅 Exhaustive=true 时客户端硬拒. maxTools, exhaustive := resolveMaxTools(req) if maxTools > 0 && exhaustive && len(req.Tools) > maxTools { if err := wire.CheckToolCount(req.Tools, maxTools); err != nil { return nil, err } } // 双开关协议(want × can)检测 - 两条 path 共用同一份 warning. // 模型是否支持 thinking 是模型级属性,和序列化格式无关. warnings := p.detectFeatureWarnings(req) var ch <-chan flyto.Event var err error switch p.cfg.Mode { case ModeAnthropic: ch, err = p.streamAnthropic(ctx, req) default: ch, err = p.streamOpenAI(ctx, req) } if err != nil { return nil, err } if len(warnings) > 0 { return prependWarnings(ch, warnings), nil } return ch, nil } // streamOpenAI 通过 Native 或 OpenAI 兼容端点流式请求. // 注:max_tools 检查 + want×can 检测已在 Stream dispatcher 层完成(PR2.6). func (p *Provider) streamOpenAI(ctx context.Context, req *flyto.Request) (<-chan flyto.Event, error) { // CAP-5: $ref 展开--MiniMax 直连有 $ref 双重序列化 bug. // 展开后 tool input 中被引用字段为 object 而非 JSON 字符串,避免静默数据损坏. // 错误时 silently skip(保留原始 schema)而非报错: // 1. 循环引用/外部 $ref 等异常 schema 应让 provider 返回明确错误,而非引擎拦截; // 2. 绝大多数工具 schema 无 $ref,快速路径保证零开销. tools := make([]flyto.Tool, len(req.Tools)) copy(tools, req.Tools) for i, t := range tools { expanded, err := wire.DereferenceSchema(t.InputSchema) if err == nil { tools[i].InputSchema = expanded } // err != nil: silently skip,保留原始 schema,让 provider 决定如何处理 tools[i].InputSchema = wire.AdaptSchema(tools[i].InputSchema, "openai") } wireReq := &wire.StreamRequest{ Model: req.Model, Messages: req.Messages, System: req.System, MaxTokens: req.MaxTokens, Tools: tools, ResponseFormat: req.ResponseFormat, Temperature: req.Temperature, TopP: req.TopP, } // MiniMax M2.1+ 支持思考模式 // Native/OpenAI 模式下通过 reasoning 参数开启(与 OpenRouter 相同字段) // NeedsThinking 支持 per-request 触发,ThinkingBudget=0 时 MaxTokens 取 Config 值(0=provider 决定). if p.cfg.ThinkingBudget > 0 || req.NeedsThinking { wireReq.Reasoning = &wire.Reasoning{ MaxTokens: p.cfg.ThinkingBudget, Enabled: true, } } return p.wireClient.Stream(ctx, wireReq) } // streamAnthropic 通过 Anthropic 兼容端点流式请求(复用 internal/api/client.go). func (p *Provider) streamAnthropic(ctx context.Context, req *flyto.Request) (<-chan flyto.Event, error) { // 历史包袱(LEGACY): Anthropic 兼容模式需要将 flyto.Request 转换为 api.MessageRequest. // 与 pkg/providers/anthropic 中的逻辑相同,但不需要处理 Anthropic 专有的 Beta headers. // 如果 MiniMax Anthropic 兼容层完全对齐 Anthropic API,可以复用 anthropic.Provider 的逻辑. // 暂时单独实现以保持独立性,避免 minimax 包依赖 anthropic 包(引入不必要的耦合). apiReq := &api.MessageRequest{ Model: req.Model, MaxTokens: req.MaxTokens, Stream: true, // MiniMax Anthropic 兼容路径直接透传 Temperature / TopP. MiniMax 文档 // 未提及与 thinking 模式的强约束 (与 Anthropic 原生 thinking 不同), // 不做 client-side override; 越界由服务端校验. // // MiniMax Anthropic-compat path passes Temperature / TopP through. // MiniMax docs do not document thinking-vs-temperature constraints // (unlike native Anthropic), so no client-side override; out-of-range // surfaces from server. Temperature: req.Temperature, TopP: req.TopP, } if req.System != "" { apiReq.SetSystemString(req.System) } budget := p.cfg.ThinkingBudget if budget == 0 && req.NeedsThinking { budget = 8000 // 与 anthropic provider 保持一致的默认值 } if budget > 0 { apiReq.Thinking = &api.ThinkingConfig{ Type: "enabled", BudgetTokens: budget, } } for _, msg := range req.Messages { role := string(msg.Role) if len(msg.Blocks) == 1 && msg.Blocks[0].Type == flyto.BlockText { apiReq.Messages = append(apiReq.Messages, api.NewTextMessage(role, msg.Blocks[0].Text)) continue } var blocks []api.ContentBlock for _, b := range msg.Blocks { switch b.Type { case flyto.BlockText: blocks = append(blocks, api.ContentBlock{Type: "text", Text: b.Text}) case flyto.BlockToolUse: blocks = append(blocks, api.ContentBlock{ Type: "tool_use", ID: b.ToolUseID, Name: b.ToolName, Input: b.ToolInput, }) case flyto.BlockToolResult: // MiniMax anthropic-compat 端点对 tool_result array-form 接受 // 未验证; 保守拒绝 ResultBlocks 形式, 让调用方切到 anthropic // provider 做 vision. 纯文本 tool_result 继续走 string path. // // Whether MiniMax anthropic-compat accepts array-form // tool_result content is unverified; conservatively reject // ResultBlocks form and route callers to the anthropic // provider for vision. Plain-text tool_result stays on the // string path. if len(b.ResultBlocks) > 0 { return nil, fmt.Errorf("minimax: tool_result with ResultBlocks not yet supported (use anthropic provider for vision)") } blocks = append(blocks, api.ContentBlock{ Type: "tool_result", ToolUseID: b.ToolUseID, Content: b.ResultText, IsError: b.IsError, }) case flyto.BlockImage: // Vision not yet wired for MiniMax anthropic-compat endpoint: // native spec support unverified, per-provider enablement // deferred. Surface as error instead of silent-dropping the // image so callers learn to select a vision-capable provider. // // MiniMax anthropic-compat 端点的 vision 支持未接: 兼容 spec // 是否真的接受 image block 未验证, 按 provider 逐个启用的节奏 // 延后. 返 error 不静默丢图, 调用方据此切换到 vision-capable // provider. return nil, fmt.Errorf("minimax: BlockImage not yet supported (use anthropic provider for vision)") } } apiReq.Messages = append(apiReq.Messages, api.NewBlockMessage(role, blocks)) } // 升华改进(ELEVATED): streamOpenAI 路径已做 DereferenceSchema + schema 适配, // Anthropic 兼容模式同样需要--MiniMax 有 $ref 双重序列化 bug, // 且 Anthropic API 不支持 additionalProperties 等非标准字段. // 替代方案:<直接透传 t.InputSchema> - 否决:已有实测证明会导致 400 错误. for _, t := range req.Tools { schema := t.InputSchema if expanded, err := wire.DereferenceSchema(schema); err == nil { schema = expanded } schema = wire.AdaptSchema(schema, "anthropic") apiReq.Tools = append(apiReq.Tools, api.ToolDef{ Name: t.Name, Description: t.Description, InputSchema: schema, }) } // 升华改进(ELEVATED): 早期方案需要 convertAnthropicStream goroutine 做中间转换-- // 现在 wire.ParseAnthropicStream + StreamGuard 在 client.CreateMessageStream 内部 // 直接产出 flyto.Event,无需额外转换层. return p.anthroClient.CreateMessageStream(ctx, apiReq) } // Models 返回 MiniMax 可用模型列表(静态表). func (p *Provider) Models(_ context.Context) ([]flyto.ModelInfo, error) { if len(p.cfg.ModelOverrides) > 0 { return p.cfg.ModelOverrides, nil } return minimaxModels, nil } // minimaxMaxTools 是 MiniMax 工具数量上限的**兜底安全值**(0 = probe 未发现上限). // probe 实测 @256 工具未触发 API 报错(2026-04). // // 升华改进(ELEVATED): data-driven-capabilities RFC PR2.6 - 主决策来自 // req.Capabilities.MaxTools.本常量仅在 Capabilities == nil 时兜底. // 当 probe 确认上限后可通过 capability-probe 写入 registry,无需改本包代码. const minimaxMaxTools = 0 // resolveMaxTools 返回当前请求的工具上限 + 是否为硬性上限(Exhaustive). // registry 优先 + 包内 minimaxMaxTools 兜底. // // 零回归:nil Capabilities → (0, false) → dispatcher 不进入 check 分支,等同 PR2.6 之前. func resolveMaxTools(req *flyto.Request) (int, bool) { if req.Capabilities != nil && req.Capabilities.MaxTools > 0 { return req.Capabilities.MaxTools, req.Capabilities.MaxToolsExhaustive } return minimaxMaxTools, false } // modelSupportsThinkingFallback 扫 minimax 静态表判断模型是否支持 thinking. // M2.7/M2.7-highspeed/M2.5/M2.1 返回 true,M2/M1 返回 false,未知模型保守返回 false. func modelSupportsThinkingFallback(model string) bool { for _, m := range minimaxModels { if m.ID == model { return m.SupportsThinking } } return false } // resolveThinkingSupport 返回当前请求模型是否支持 thinking.registry 优先 + 静态表兜底. func resolveThinkingSupport(req *flyto.Request) bool { if req.Capabilities != nil { return req.Capabilities.SupportsThinking } return modelSupportsThinkingFallback(req.Model) } // detectFeatureWarnings 检测 want × can 不一致的场景,返回 WarningEvent 列表. // // 升华改进(ELEVATED): data-driven-capabilities RFC §4.4 双开关协议. // minimax 两条 stream 路径(streamOpenAI / streamAnthropic)原本都**无条件**注入 thinking // 参数,不检查模型是否支持.PR2.6 保留无条件注入(零回归),但通过 WarningEvent 检测 // user opt-in 但模型不支持的场景,让 silent disable 变 loud. // // minimax 两条路径共用同一份 warning - 因为"模型是否支持 thinking"是模型级属性,和 // 序列化格式(OpenAI vs Anthropic)无关.因此本 helper 放在 Stream dispatcher 层, // 避免两条 path 重复调用. // // minimax 当前仅检测 thinking;Config 无 EnableCaching 字段(caching 在 wire 层自动处理), // 所以 caching 的 want 永远为 false,不触发检测. func (p *Provider) detectFeatureWarnings(req *flyto.Request) []*flyto.WarningEvent { var warnings []*flyto.WarningEvent wantsThinking := p.cfg.ThinkingBudget > 0 || req.NeedsThinking if wantsThinking && !resolveThinkingSupport(req) { warnings = append(warnings, &flyto.WarningEvent{ Code: "feature_unsupported", Message: "Config.ThinkingBudget/NeedsThinking 已设置但模型 " + req.Model + " 不支持 thinking,thinking 可能被 API 忽略", Detail: "model=" + req.Model + " feature=thinking", }) } return warnings } // prependWarnings 在下游 channel 前面 prepend 一组 WarningEvent. // 参见 anthropic/ollama/openai/gemini/openrouter 同款实现;RFC §3 决策不抽共享包. func prependWarnings(downstream <-chan flyto.Event, warnings []*flyto.WarningEvent) <-chan flyto.Event { out := make(chan flyto.Event, len(warnings)) for _, w := range warnings { out <- w } go func() { defer close(out) for evt := range downstream { out <- evt } }() return out } // minimaxModels 是 MiniMax 已知模型的静态表(2026-04 更新). // 所有模型上下文窗口为 200K(MiniMax-M1 为 1M),无 batch API. var minimaxModels = []flyto.ModelInfo{ { ID: "MiniMax-M2.7", DisplayName: "MiniMax M2.7", Provider: "minimax", ContextWindow: 205_000, MaxOutputTokens: 16_000, InputPricePer1M: 0.30, OutputPricePer1M: 1.20, SupportsCaching: true, SupportsThinking: true, SupportsVision: true, // probe 实测:cr=1110 rd=1110 @ 100 reps(~1000t 系统提示),阈值 ≤ 1024. CachingMinTokens: 1024, MaxTools: 0, // probe 实测 @256 未发现上限(2026-04),待后续验证 }, { // 升华改进(ELEVATED): M2.7-highspeed 是 M2.7 的快速变体,2026-04 capability-probe // 实测能力与 M2.7 完全一致(streaming/thinking/tool_use/structured_out/caching cr=1110 rd=1110/schema_ref/tool_count≥128). // 静态表镜像 M2.7 的所有字段,仅 ID/DisplayName 不同;如官方公布 highspeed 专属定价或上下文差异,再单独调整. // 替代方案:<不收录,等用户显式 New(ModelOverrides=...) 注入> - 否决:probe 已主动测试此 ID, // Models() 返回结果若不包含此变体,定价/进度条等下游消费者会无法识别,造成 UI 断码. ID: "MiniMax-M2.7-highspeed", DisplayName: "MiniMax M2.7 (Highspeed)", Provider: "minimax", ContextWindow: 205_000, MaxOutputTokens: 16_000, InputPricePer1M: 0.30, OutputPricePer1M: 1.20, SupportsCaching: true, SupportsThinking: true, SupportsVision: true, CachingMinTokens: 1024, MaxTools: 0, }, { ID: "MiniMax-M2.5", DisplayName: "MiniMax M2.5", Provider: "minimax", ContextWindow: 200_000, MaxOutputTokens: 16_000, InputPricePer1M: 0.118, OutputPricePer1M: 0.95, SupportsCaching: true, SupportsThinking: true, SupportsVision: true, CachingMinTokens: 1024, // 与 M2.7 同系列,预期阈值相同;未单独 probe MaxTools: 0, // probe 实测 @256 未发现上限(2026-04) }, { ID: "MiniMax-M2.1", DisplayName: "MiniMax M2.1", Provider: "minimax", ContextWindow: 200_000, MaxOutputTokens: 16_000, InputPricePer1M: 0.27, OutputPricePer1M: 0.95, SupportsCaching: true, SupportsThinking: true, SupportsVision: false, CachingMinTokens: 1024, MaxTools: 0, // probe 实测 @256 未发现上限(2026-04) }, { ID: "MiniMax-M2", DisplayName: "MiniMax M2", Provider: "minimax", ContextWindow: 200_000, MaxOutputTokens: 16_000, InputPricePer1M: 0.255, OutputPricePer1M: 1.00, SupportsCaching: true, SupportsThinking: false, SupportsVision: false, CachingMinTokens: 1024, MaxTools: 0, // probe 实测 @256 未发现上限(2026-04) }, { ID: "MiniMax-M1", DisplayName: "MiniMax M1 (1M context)", Provider: "minimax", ContextWindow: 1_000_000, MaxOutputTokens: 16_000, InputPricePer1M: 0.40, OutputPricePer1M: 1.76, SupportsCaching: false, SupportsThinking: false, SupportsVision: false, CachingMinTokens: 0, MaxTools: 0, // probe 实测 @256 未发现上限(2026-04) }, }