| 1 | package provider |
| 2 | |
| 3 | import "testing" |
| 4 | |
| 5 | func TestOpenCodeGoChatModelsMatchPinnedLimits(t *testing.T) { |
| 6 | want := map[string]OpenCodeGoModelLimits{ |
| 7 | "glm-5.3": {Context: 1_000_000, MaxOutput: 131_072}, |
| 8 | "glm-5.2": {Context: 1_000_000, MaxOutput: 131_072}, |
| 9 | "glm-5.1": {Context: 202_752, MaxOutput: 32_768}, |
| 10 | "kimi-k3": {Context: 1_048_576, MaxOutput: 131_072}, |
| 11 | "kimi-k2.7-code": {Context: 262_144, MaxOutput: 262_144}, |
| 12 | "kimi-k2.6": {Context: 262_144, MaxOutput: 65_536}, |
| 13 | "deepseek-v4-pro": {Context: 1_000_000, MaxOutput: 384_000}, |
| 14 | "deepseek-v4-flash": {Context: 1_000_000, MaxOutput: 384_000}, |
| 15 | "deepseek-v4-flash-vision-exp": {Context: 1_000_000, MaxOutput: 384_000}, |
| 16 | "mimo-v2.5-pro": {Context: 1_048_576, MaxOutput: 128_000}, |
| 17 | "mimo-v2.5": {Context: 1_000_000, MaxOutput: 128_000}, |
| 18 | "hy3": {Context: 256_000, MaxOutput: 64_000}, |
| 19 | } |
| 20 | got := OpenCodeGoChatModels() |
| 21 | for id, lim := range want { |
| 22 | if got[id] != lim { |
| 23 | t.Fatalf("%s = %+v, want %+v", id, got[id], lim) |
| 24 | } |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | func TestOpenCodeGoAnthropicModelsMatchPinnedLimits(t *testing.T) { |
| 29 | want := map[string]OpenCodeGoModelLimits{ |
| 30 | "qwen3.8-max": {Context: 1_000_000, MaxOutput: 131_072}, |
| 31 | "qwen3.7-max": {Context: 1_000_000, MaxOutput: 65_536}, |
| 32 | "qwen3.7-plus": {Context: 1_000_000, MaxOutput: 65_536}, |
| 33 | "qwen3.6-plus": {Context: 1_000_000, MaxOutput: 65_536}, |
| 34 | "minimax-m3": {Context: 1_000_000, MaxOutput: 131_072}, |
| 35 | "minimax-m2.7": {Context: 204_800, MaxOutput: 131_072}, |
| 36 | "minimax-m2.5": {Context: 204_800, MaxOutput: 65_536}, |
| 37 | } |
| 38 | got := OpenCodeGoAnthropicModels() |
| 39 | for id, lim := range want { |
| 40 | if got[id] != lim { |
| 41 | t.Fatalf("%s = %+v, want %+v", id, got[id], lim) |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | func TestOpenCodeGoResponsesModelsMatchPinnedLimits(t *testing.T) { |
| 47 | want := map[string]OpenCodeGoModelLimits{ |
| 48 | "grok-4.5": {Context: 500_000, MaxOutput: 500_000}, |
| 49 | "gpt-5.6-luna": {Context: 1_050_000, MaxOutput: 128_000}, |
| 50 | "muse-spark-1.2-contributor": {Context: 1_048_576, MaxOutput: 131_072}, |
| 51 | } |
| 52 | got := OpenCodeGoResponsesModels() |
| 53 | for id, lim := range want { |
| 54 | if got[id] != lim { |
| 55 | t.Fatalf("%s = %+v, want %+v", id, got[id], lim) |
| 56 | } |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | func TestFilterOfficialOpenCodeGoModelsKeepsOnlyRouteCompatibleModels(t *testing.T) { |
| 61 | all := []string{"grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor", "glm-5.3", "glm-5.2", "hy3", "qwen3.8-max", "qwen3.7-plus", "deepseek-flash", "deepseek-v4-flash", "unknown-future-model"} |
| 62 | tests := []struct { |
| 63 | name string |
| 64 | kind string |
| 65 | baseURL string |
| 66 | want []string |
| 67 | }{ |
| 68 | {name: "chat", kind: "openai", baseURL: "https://opencode.ai/zen/go/v1", want: []string{"glm-5.3", "glm-5.2", "hy3", "qwen3.8-max", "qwen3.7-plus", "deepseek-flash", "deepseek-v4-flash"}}, |
| 69 | {name: "anthropic", kind: "anthropic", baseURL: "https://opencode.ai/zen/go", want: []string{"qwen3.8-max", "qwen3.7-plus", "deepseek-flash", "deepseek-v4-flash"}}, |
| 70 | {name: "responses", kind: "responses", baseURL: "https://opencode.ai/zen/go/v1", want: []string{"grok-4.5", "gpt-5.6-luna", "muse-spark-1.2-contributor", "deepseek-flash", "deepseek-v4-flash"}}, |
| 71 | {name: "custom endpoint is untouched", kind: "anthropic", baseURL: "https://relay.example/zen/go", want: all}, |
| 72 | } |
| 73 | |
| 74 | for _, tt := range tests { |
| 75 | t.Run(tt.name, func(t *testing.T) { |
| 76 | got := FilterOfficialOpenCodeGoModels(tt.kind, tt.baseURL, all) |
| 77 | if len(got) != len(tt.want) { |
| 78 | t.Fatalf("filtered models = %v, want %v", got, tt.want) |
| 79 | } |
| 80 | for i := range got { |
| 81 | if got[i] != tt.want[i] { |
| 82 | t.Fatalf("filtered models = %v, want %v", got, tt.want) |
| 83 | } |
| 84 | } |
| 85 | }) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func TestLookupOfficialOpenCodeGoRejectsLookalikes(t *testing.T) { |
| 90 | if _, ok := LookupOfficialOpenCodeGo("openai", "https://opencode.ai.attacker.example/zen/go/v1", "kimi-k3"); ok { |
| 91 | t.Fatal("lookalike host must not match") |
| 92 | } |
| 93 | if _, ok := LookupOfficialOpenCodeGo("openai", "https://opencode.ai/zen/go/v1?x=1", "kimi-k3"); ok { |
| 94 | t.Fatal("query string must not match") |
| 95 | } |
| 96 | if _, ok := LookupOfficialOpenCodeGo("openai", "http://opencode.ai/zen/go/v1", "kimi-k3"); ok { |
| 97 | t.Fatal("http must not match") |
| 98 | } |
| 99 | if _, ok := LookupOfficialOpenCodeGo("openai", "https://opencode.ai/zen/go/v1", "future-model"); ok { |
| 100 | t.Fatal("unknown model must not assume limits") |
| 101 | } |
| 102 | if _, ok := LookupOfficialOpenCodeGo("openai", "https://gateway.example/zen/go/v1", "kimi-k3"); ok { |
| 103 | t.Fatal("custom proxy must not match") |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | func TestLookupOfficialOpenCodeGoKnownRoutes(t *testing.T) { |
| 108 | chat, ok := LookupOfficialOpenCodeGo("openai", "https://opencode.ai/zen/go/v1", "kimi-k3") |
| 109 | if !ok || chat.Context != 1_048_576 || chat.MaxOutput != 131_072 { |
| 110 | t.Fatalf("chat kimi-k3 = %+v ok=%v", chat, ok) |
| 111 | } |
| 112 | anth, ok := LookupOfficialOpenCodeGo("anthropic", "https://opencode.ai/zen/go", "qwen3.7-plus") |
| 113 | if !ok || anth.MaxOutput != 65_536 { |
| 114 | t.Fatalf("anthropic qwen = %+v ok=%v", anth, ok) |
| 115 | } |
| 116 | resp, ok := LookupOfficialOpenCodeGo("responses", "https://opencode.ai/zen/go/v1", "deepseek-v4-flash") |
| 117 | if !ok || resp.MaxOutput != 384_000 { |
| 118 | t.Fatalf("responses flash = %+v ok=%v", resp, ok) |
| 119 | } |
| 120 | grok, ok := LookupOfficialOpenCodeGo("responses", "https://opencode.ai/zen/go/v1", "grok-4.5") |
| 121 | if !ok || grok.Context != 500_000 || grok.MaxOutput != 500_000 { |
| 122 | t.Fatalf("responses grok = %+v ok=%v", grok, ok) |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | func TestOpenCodeGoModelInfoUsesExactLocalCatalog(t *testing.T) { |
| 127 | vision, ok := OpenCodeGoModelInfo("openai", "https://opencode.ai/zen/go/v1", "kimi-k3") |
| 128 | if !ok || !vision.SupportsInput(ModalityImage) { |
| 129 | t.Fatalf("kimi-k3 metadata = %+v, ok=%t", vision, ok) |
| 130 | } |
| 131 | text, ok := OpenCodeGoModelInfo("openai", "https://opencode.ai/zen/go/v1", "glm-5.2") |
| 132 | if !ok || text.SupportsInput(ModalityImage) || len(text.InputModalities) != 1 || text.InputModalities[0] != ModalityText { |
| 133 | t.Fatalf("glm-5.2 metadata = %+v, ok=%t", text, ok) |
| 134 | } |
| 135 | if _, ok := OpenCodeGoModelInfo("openai", "https://opencode.ai/zen/go/v1", "omen-alpha"); ok { |
| 136 | t.Fatal("uncatalogued model must not be inferred from its endpoint") |
| 137 | } |
| 138 | kimi, ok := OpenCodeGoModelInfo("openai", "https://opencode.ai/zen/go/v1", "kimi-k2.6") |
| 139 | if !ok || !kimi.SupportsInput(ModalityImage) { |
| 140 | t.Fatalf("pi catalog kimi-k2.6 metadata = %+v, ok=%t", kimi, ok) |
| 141 | } |
| 142 | if kimi.ContextWindow == 0 || kimi.MaxOutputTokens == 0 || kimi.API == "" { |
| 143 | t.Fatalf("pi catalog should preserve model metadata, got %+v", kimi) |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | func TestPiCatalogModelInfosContainsMultipleProviders(t *testing.T) { |
| 148 | for _, id := range []string{"opencode-go", "deepseek", "anthropic", "openai"} { |
| 149 | if models := PiCatalogModelInfos(id); len(models) == 0 { |
| 150 | t.Fatalf("pi catalog provider %q is empty", id) |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | func TestPiCatalogModelInfoForProviderRequiresExactServingRoute(t *testing.T) { |
| 156 | model, ok := PiCatalogModelInfoForProvider("opencode-go", "openai", "https://opencode.ai/zen/go/v1", "qwen3.6-plus") |
| 157 | if !ok || !model.SupportsInput(ModalityImage) || model.API != "openai-completions" { |
| 158 | t.Fatalf("OpenCode catalog model = %+v, ok=%t", model, ok) |
| 159 | } |
| 160 | if _, ok := PiCatalogModelInfoForProvider("opencode-go", "openai", "https://gateway.example/v1", "qwen3.6-plus"); ok { |
| 161 | t.Fatal("custom endpoint must not inherit pi catalog metadata") |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | func TestPiCatalogContractKeepsKnownCapabilityFacts(t *testing.T) { |
| 166 | cases := []struct { |
| 167 | kind, baseURL, model string |
| 168 | image bool |
| 169 | }{ |
| 170 | {"openai", "https://opencode.ai/zen/go/v1", "kimi-k3", true}, |
| 171 | {"openai", "https://opencode.ai/zen/go/v1", "glm-5.2", false}, |
| 172 | {"openai", "https://opencode.ai/zen/go/v1", "deepseek-v4-flash-vision-exp", true}, |
| 173 | {"anthropic", "https://opencode.ai/zen/go", "qwen3.8-flash", true}, |
| 174 | {"responses", "https://opencode.ai/zen/go/v1", "grok-4.6", true}, |
| 175 | } |
| 176 | for _, tc := range cases { |
| 177 | info, ok := PiCatalogModelInfo(tc.kind, tc.baseURL, tc.model) |
| 178 | if !ok || info.SupportsInput(ModalityImage) != tc.image { |
| 179 | t.Fatalf("pi catalog contract %s/%s = %+v, ok=%t", tc.kind, tc.model, info, ok) |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | func TestModelScopeModelInfoUsesVerifiedLocalCatalog(t *testing.T) { |
| 185 | vision, ok := ModelScopeModelInfo("openai", "https://api-inference.modelscope.cn/v1", "Qwen/Qwen3.5-27B") |
| 186 | if !ok || !vision.SupportsInput(ModalityImage) { |
| 187 | t.Fatalf("ModelScope vision metadata = %+v, ok=%t", vision, ok) |
| 188 | } |
| 189 | text, ok := ModelScopeModelInfo("openai", "https://api-inference.modelscope.cn/v1", "ZhipuAI/GLM-5.2") |
| 190 | if !ok || text.SupportsInput(ModalityImage) { |
| 191 | t.Fatalf("ModelScope text metadata = %+v, ok=%t", text, ok) |
| 192 | } |
| 193 | if _, ok := ModelScopeModelInfo("openai", "https://gateway.example/v1", "Qwen/Qwen3.5-27B"); ok { |
| 194 | t.Fatal("custom ModelScope lookalike must not use the local catalog") |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | func TestBuiltinModelInfoIncludesDeepSeekVisionSKU(t *testing.T) { |
| 199 | vision, ok := BuiltinModelInfo("openai", "https://api.deepseek.com/v1", "deepseek-v4-flash-vision-exp") |
| 200 | if !ok || !vision.SupportsInput(ModalityImage) { |
| 201 | t.Fatalf("DeepSeek vision metadata = %+v, ok=%t", vision, ok) |
| 202 | } |
| 203 | text, ok := BuiltinModelInfo("openai", "https://api.deepseek.com/v1", "deepseek-v4-pro") |
| 204 | if !ok || text.SupportsInput(ModalityImage) { |
| 205 | t.Fatalf("DeepSeek text metadata = %+v, ok=%t", text, ok) |
| 206 | } |
| 207 | } |
| 208 |