package openai import ( "strings" "testing" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // makeTestTools 创建指定数量的占位工具(测试辅助). func makeTestTools(n int) []flyto.Tool { tools := make([]flyto.Tool, n) for i := range tools { tools[i] = flyto.Tool{Name: "tool", Description: "test"} } return tools } // TestStream_ToolCountLimit 验证 CAP-7:超出 OpenAI 工具数量上限(128)时 Stream 返回错误. func TestStream_ToolCountLimit(t *testing.T) { p := New(Config{APIKey: "test-key"}) // 工具数量恰好在上限时不应报错(CheckToolCount 不会进入 Stream 底层调用,直到 HTTP 层) // 注意:此处不真正调用 API,仅验证工具数量检查前置逻辑. // 129 个工具 → 应在 CheckToolCount 阶段报错,不发出任何 HTTP 请求. req := &flyto.Request{ Model: "gpt-4o", Tools: makeTestTools(openaiMaxTools + 1), System: "test", MaxTokens: 100, } _, err := p.Stream(t.Context(), req) if err == nil { t.Fatal("expected error for tool count exceeding OpenAI limit, got nil") } if !strings.Contains(err.Error(), "129") { t.Errorf("error should mention actual count 129, got: %v", err) } if !strings.Contains(err.Error(), "128") { t.Errorf("error should mention limit 128, got: %v", err) } } // TestStream_ToolCountAtLimit 验证工具数量恰好等于上限(128)时不报工具数量错误. // 此时错误来自 HTTP 层(网络不通),不是 CheckToolCount. func TestStream_ToolCountAtLimit(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "gpt-4o", Tools: makeTestTools(openaiMaxTools), // 128,恰好在上限 MaxTokens: 100, } _, err := p.Stream(t.Context(), req) // 允许报错(网络层),但不能是工具数量超限错误 if err != nil && strings.Contains(err.Error(), "128") && strings.Contains(err.Error(), "exceeds") { t.Errorf("at-limit tool count should not trigger CAP-7 error, got: %v", err) } } // === PR2.3 data-driven-capabilities: registry 优先 + 包内兜底 === func TestResolveMaxTools_NilCapabilities(t *testing.T) { req := &flyto.Request{Model: "gpt-4o"} if mt := resolveMaxTools(req); mt != openaiMaxTools { t.Errorf("nil Capabilities 应兜底到 openaiMaxTools=%d,实际 %d", openaiMaxTools, mt) } } func TestResolveMaxTools_RegistryOverride(t *testing.T) { // registry 可注入更新的上限(比如 OpenAI 未来放宽到 256) req := &flyto.Request{ Model: "gpt-4o", Capabilities: &flyto.ModelInfo{MaxTools: 256}, } if mt := resolveMaxTools(req); mt != 256 { t.Errorf("registry 注入 256 应返回 256,实际 %d", mt) } } func TestResolveMaxTools_RegistryZeroFallsBack(t *testing.T) { // MaxTools=0 表示 registry 无数据 → 走兜底 req := &flyto.Request{ Model: "gpt-4o", Capabilities: &flyto.ModelInfo{MaxTools: 0}, } if mt := resolveMaxTools(req); mt != openaiMaxTools { t.Errorf("MaxTools=0 应降级到兜底 %d,实际 %d", openaiMaxTools, mt) } } // 验证 openai 的 max_tools 永远硬拒(不走软处理) - 即使 registry 标 Exhaustive=false. // 这是 openai 相对通用模板的特化:OpenAI API 级上限永远硬性. func TestStream_RegistryExhaustiveFalseStillHardRejects(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "gpt-4o", Tools: makeTestTools(6), // 超过 registry 的 5 Capabilities: &flyto.ModelInfo{ MaxTools: 5, MaxToolsExhaustive: false, // 即使标 false,openai 仍硬拒 }, MaxTokens: 100, } _, err := p.Stream(t.Context(), req) if err == nil { t.Fatal("openai 应硬拒超限请求,即使 Exhaustive=false") } if !strings.Contains(err.Error(), "5") { t.Errorf("error 应含 limit 5: %v", err) } } func TestModelSupportsThinkingFallback(t *testing.T) { // o1/o3/o3-mini 支持 thinking,其他模型不支持 cases := map[string]bool{ "o1": true, "o3": true, "o3-mini": true, "gpt-4o": false, "gpt-4o-mini": false, "gpt-4.1": false, "gpt-5": false, "unknown": false, } for model, want := range cases { if got := modelSupportsThinkingFallback(model); got != want { t.Errorf("modelSupportsThinkingFallback(%q) = %v, want %v", model, got, want) } } } func TestResolveThinkingSupport_NilFallsBackToStaticTable(t *testing.T) { // nil Capabilities → 扫静态表 req := &flyto.Request{Model: "o1"} if !resolveThinkingSupport(req) { t.Error("nil Capabilities + o1 静态表应返回 true") } req2 := &flyto.Request{Model: "gpt-4o"} if resolveThinkingSupport(req2) { t.Error("nil Capabilities + gpt-4o 静态表应返回 false") } } func TestResolveThinkingSupport_RegistryOverridesStaticTable(t *testing.T) { // registry 数据优先级高于静态表 req := &flyto.Request{ Model: "gpt-4o", Capabilities: &flyto.ModelInfo{SupportsThinking: true}, // 相信 registry } if !resolveThinkingSupport(req) { t.Error("registry SupportsThinking=true 应优先于静态表 false") } } // === 双开关协议(want × can)4 组合 === func TestDetectWarnings_WantFalse_CanFalse(t *testing.T) { p := New(Config{}) req := &flyto.Request{Model: "gpt-4o", NeedsThinking: false} if w := p.detectFeatureWarnings(req); len(w) != 0 { t.Errorf("want=false can=false 应零 warning,实际 %d 个", len(w)) } } func TestDetectWarnings_WantFalse_CanTrue(t *testing.T) { p := New(Config{}) req := &flyto.Request{Model: "o1", NeedsThinking: false} if w := p.detectFeatureWarnings(req); len(w) != 0 { t.Errorf("want=false can=true 应零 warning(尊重 opt-out),实际 %d 个", len(w)) } } func TestDetectWarnings_WantTrue_CanFalse(t *testing.T) { // 关键场景:gpt-4o 不支持 thinking,但用户 NeedsThinking=true → silent disable p := New(Config{}) req := &flyto.Request{Model: "gpt-4o", NeedsThinking: true} warnings := p.detectFeatureWarnings(req) if len(warnings) != 1 { t.Fatalf("want=true can=false 应发 1 个 warning,实际 %d 个", len(warnings)) } w := warnings[0] if w.Code != "feature_unsupported" { t.Errorf("warning Code = %q, want feature_unsupported", w.Code) } if !strings.Contains(w.Message, "gpt-4o") { t.Errorf("warning Message 应含模型名: %q", w.Message) } if !strings.Contains(w.Detail, "feature=thinking") { t.Errorf("warning Detail 应含 feature=thinking: %q", w.Detail) } } func TestDetectWarnings_WantTrue_CanTrue(t *testing.T) { // o1 支持 thinking,用户 NeedsThinking=true → 不发 warning p := New(Config{}) req := &flyto.Request{Model: "o1", NeedsThinking: true} if w := p.detectFeatureWarnings(req); len(w) != 0 { t.Errorf("want=true can=true 应零 warning,实际 %d 个", len(w)) } } // TestModels_MaxTools 验证静态模型表中每个模型都正确设置了 MaxTools. func TestModels_MaxTools(t *testing.T) { p := New(Config{}) models, err := p.Models(t.Context()) if err != nil { t.Fatalf("Models() error: %v", err) } if len(models) == 0 { t.Fatal("expected non-empty model list") } for _, m := range models { if m.MaxTools != openaiMaxTools { t.Errorf("model %s: MaxTools=%d, want %d", m.ID, m.MaxTools, openaiMaxTools) } } }