// Package wire/tools.go - Tool 数量上限保护 // // 背景(BUGFIX): OpenAI Chat API ≤ 128 个工具(OpenAPI spec 明确), // Anthropic strict ≤ 20,超出直接 400 且错误信息晦涩. // 在发送前做前置检查,把"provider 500/400 含义不明"变成"引擎侧明确错误". // // 精妙之处(CLEVER): 不截断,让消费者自己决定裁剪哪些工具-- // 截断会改变 agent 的能力语义,引擎无法判断哪些工具重要,哪些可丢弃. // 报错让消费者在正确的抽象层级做决策. // // 替代方案:<截断到 max,多余工具静默丢弃> - 否决: // Agent 场景下工具缺失会导致静默功能降级,比 400 错误更难 debug. package wire import ( "fmt" "regexp" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/flyto" ) // CheckToolCount 验证工具数量不超过 provider 上限. // // max <= 0 时直接返回 nil(表示未知/无限制,跳过检查). // len(tools) > max 时返回格式化错误,消费者应在此错误前裁剪工具列表. // // 已知上限(2026-04): // - OpenAI Chat API : 128(OpenAPI spec 明确记录) // - Anthropic strict: 20(官方文档;非 strict 无明确上限) // - MiniMax : 0(probe 实测 @256 未发现上限,待验证) // - OpenRouter : 0(透传底层模型,自身无额外限制) func CheckToolCount(tools []flyto.Tool, max int) error { if max <= 0 { // 0 = 未知/无限制,跳过检查 return nil } if len(tools) > max { return fmt.Errorf("tool count %d exceeds provider limit of %d", len(tools), max) } return nil } // ValidateToolNames pre-flight 校验每个 tool.Name 是否命中 provider // 强制要求的 regex pattern. // // ADR-0007 (Bug U follow-up, 2026-05-01): r22 实证 OpenRouter→ // SiliconFlow 拒 "billcost.reflect" (含 dot) HTTP 400 + 错误信息只在 // nested metadata.raw 里. wire 层 pre-flight 让违规 tool 名在请求发出 // 前就被 typed error 捕获, 比让 provider 4xx 自然冒泡更快 + Detail // 信息明确. // // regex 空 = 未知 / 不强制 = 跳过校验 (零回归; 保留旧行为让 provider // 4xx 自然走 ADR-0006 typed error 路径). // // regex 非空: 编译失败 silent skip (防御性, regex 错误不阻塞业务, // 由消费者 ModelInfo 修); 任意 tool 名 mismatch 返 // flyto.EngineError(ErrModelToolUnsupported, Message + Detail) 让 // engine retry 层 errors.As 命中 typed code. // // ValidateToolNames pre-flight 校验每个 tool.Name 是否命中 provider // 强制 regex. 违规返 ErrModelToolUnsupported typed error 早期拒绝. // regex 空 = 跳过 (零回归). func ValidateToolNames(tools []flyto.Tool, regex string) error { if regex == "" { return nil } re, err := regexp.Compile(regex) if err != nil { // regex 错误属消费者 ModelInfo 配置问题, 不阻塞业务. 静默 // fallback 让 provider 4xx 自然冒泡 (ADR-0006 路径覆盖). // // regex compile 失败 silent skip, 防御性回退让 provider 4xx // 自然冒泡 (ADR-0006 typed error 兜). return nil } for _, t := range tools { if t.Name == "" { continue } if !re.MatchString(t.Name) { return &flyto.EngineError{ Code: flyto.ErrModelToolUnsupported, Message: "wire: tool name violates provider regex", Detail: fmt.Sprintf("tool name %q does not match required pattern %q", t.Name, regex), } } } return nil }