| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "reasonix/internal/provider/openai" |
| 7 | ) |
| 8 | |
| 9 | func TestEffectiveVisionEnablesPinnedOfficialDeepSeekVisionSKU(t *testing.T) { |
| 10 | sku := &ProviderEntry{ |
| 11 | Name: "deepseek", |
| 12 | Kind: "openai", |
| 13 | BaseURL: "https://api.deepseek.com", |
| 14 | Model: openai.OfficialDeepSeekVisionModel, |
| 15 | } |
| 16 | if !CanConfigureVision(sku) { |
| 17 | t.Fatal("official DeepSeek Settings must expose per-model image-input checkboxes") |
| 18 | } |
| 19 | if !EffectiveVision(sku) || !ExplicitModelVision(sku) { |
| 20 | t.Fatal("selecting the pinned official DeepSeek vision SKU must enable image input") |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | func TestEffectiveVisionHonorsOfficialDeepSeekVisionModels(t *testing.T) { |
| 25 | sku := &ProviderEntry{ |
| 26 | Name: "deepseek", |
| 27 | Kind: "openai", |
| 28 | BaseURL: "https://api.deepseek.com", |
| 29 | Model: openai.OfficialDeepSeekVisionModel, |
| 30 | VisionModels: []string{openai.OfficialDeepSeekVisionModel}, |
| 31 | } |
| 32 | if !EffectiveVision(sku) || !ExplicitModelVision(sku) { |
| 33 | t.Fatal("checking image input on the official vision SKU must enable image input") |
| 34 | } |
| 35 | |
| 36 | sku.VisionModels = []string{} |
| 37 | if EffectiveVision(sku) || ExplicitModelVision(sku) { |
| 38 | t.Fatal("unchecking image input must disable image input on the official vision SKU") |
| 39 | } |
| 40 | |
| 41 | pro := &ProviderEntry{ |
| 42 | Name: "deepseek", |
| 43 | Kind: "openai", |
| 44 | BaseURL: "https://api.deepseek.com", |
| 45 | Model: "deepseek-v4-pro", |
| 46 | VisionModels: []string{"deepseek-v4-pro", openai.OfficialDeepSeekVisionModel}, |
| 47 | } |
| 48 | if EffectiveVision(pro) || ExplicitModelVision(pro) { |
| 49 | t.Fatal("checking image input on a text-only model must not enable official DeepSeek image payloads") |
| 50 | } |
| 51 | |
| 52 | // V4.1 Flash is natively multimodal, so a curated list that predates it must |
| 53 | // not veto it — while an explicitly emptied list still turns images off. |
| 54 | flash := &ProviderEntry{ |
| 55 | Name: "deepseek", |
| 56 | Kind: "openai", |
| 57 | BaseURL: "https://api.deepseek.com", |
| 58 | Model: "deepseek-flash", |
| 59 | VisionModels: []string{openai.OfficialDeepSeekVisionModel}, |
| 60 | } |
| 61 | if !EffectiveVision(flash) { |
| 62 | t.Fatal("a stale curated vision list vetoed a natively multimodal model") |
| 63 | } |
| 64 | flash.VisionModels = []string{} |
| 65 | if EffectiveVision(flash) { |
| 66 | t.Fatal("an explicitly emptied vision list must disable image input") |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | func TestNormalizeOfficialDeepSeekModelsBackfillsVisionSKUOnStockCatalog(t *testing.T) { |
| 71 | c := &Config{Providers: []ProviderEntry{{ |
| 72 | Name: "deepseek", |
| 73 | Kind: "anthropic", |
| 74 | BaseURL: "https://api.deepseek.com/anthropic", |
| 75 | Models: []string{"deepseek-v4-flash", "deepseek-v4-pro"}, |
| 76 | Default: "deepseek-v4-flash", |
| 77 | APIKeyEnv: "DEEPSEEK_API_KEY", |
| 78 | Prices: DeepSeekV4PricesForCurrency("USD"), |
| 79 | }}} |
| 80 | normalizeOfficialDeepSeekModels(c) |
| 81 | p, ok := c.Provider("deepseek") |
| 82 | if !ok { |
| 83 | t.Fatal("deepseek provider missing") |
| 84 | } |
| 85 | if !p.HasModel(openai.OfficialDeepSeekVisionModel) { |
| 86 | t.Fatalf("stock official catalog = %v, want pinned vision SKU", p.ModelList()) |
| 87 | } |
| 88 | if p.Default != "deepseek-v4-flash" { |
| 89 | t.Fatalf("default = %q, want flash after vision backfill", p.Default) |
| 90 | } |
| 91 | if !p.HasVisionModel(openai.OfficialDeepSeekVisionModel) { |
| 92 | t.Fatalf("vision_models = %v, want pinned vision SKU", p.VisionModels) |
| 93 | } |
| 94 | flash := p.Prices["deepseek-v4-flash"] |
| 95 | got := p.Prices[openai.OfficialDeepSeekVisionModel] |
| 96 | if flash == nil || got == nil || got.CacheHit != flash.CacheHit || got.Input != flash.Input || got.Output != flash.Output || got.Currency != flash.Currency { |
| 97 | t.Fatalf("vision SKU price = %+v, want Flash table %+v", got, flash) |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func TestNormalizeOfficialDeepSeekModelsBackfillsVisionOnFlashProvider(t *testing.T) { |
| 102 | c := &Config{Providers: []ProviderEntry{{ |
| 103 | Name: "deepseek-flash", |
| 104 | Kind: "openai", |
| 105 | BaseURL: "https://api.deepseek.com", |
| 106 | Models: []string{"deepseek-v4-flash"}, |
| 107 | Default: "deepseek-v4-flash", |
| 108 | APIKeyEnv: "DEEPSEEK_API_KEY", |
| 109 | }}} |
| 110 | normalizeOfficialDeepSeekModels(c) |
| 111 | p, ok := c.Provider("deepseek-flash") |
| 112 | if !ok { |
| 113 | t.Fatal("deepseek-flash provider missing") |
| 114 | } |
| 115 | if !p.HasModel("deepseek-v4-flash") || !p.HasModel(openai.OfficialDeepSeekVisionModel) || p.HasModel("deepseek-v4-pro") { |
| 116 | t.Fatalf("deepseek-flash models = %v, want flash + vision SKU", p.ModelList()) |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | func TestNormalizeOfficialDeepSeekModelsSkipsVisionOnProProvider(t *testing.T) { |
| 121 | c := &Config{Providers: []ProviderEntry{{ |
| 122 | Name: "deepseek-pro", |
| 123 | Kind: "openai", |
| 124 | BaseURL: "https://api.deepseek.com", |
| 125 | Models: []string{"deepseek-v4-pro"}, |
| 126 | Default: "deepseek-v4-pro", |
| 127 | APIKeyEnv: "DEEPSEEK_API_KEY", |
| 128 | }}} |
| 129 | normalizeOfficialDeepSeekModels(c) |
| 130 | p, ok := c.Provider("deepseek-pro") |
| 131 | if !ok { |
| 132 | t.Fatal("deepseek-pro provider missing") |
| 133 | } |
| 134 | if p.HasModel(openai.OfficialDeepSeekVisionModel) { |
| 135 | t.Fatalf("deepseek-pro models = %v, want pro only", p.ModelList()) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func TestNormalizeOfficialDeepSeekModelsPreservesExplicitEmptyVisionModels(t *testing.T) { |
| 140 | c := &Config{Providers: []ProviderEntry{{ |
| 141 | Name: "deepseek", |
| 142 | Kind: "openai", |
| 143 | BaseURL: "https://api.deepseek.com", |
| 144 | Models: []string{"deepseek-v4-flash", "deepseek-v4-pro"}, |
| 145 | Default: "deepseek-v4-flash", |
| 146 | VisionModels: []string{}, |
| 147 | APIKeyEnv: "DEEPSEEK_API_KEY", |
| 148 | }}} |
| 149 | normalizeOfficialDeepSeekModels(c) |
| 150 | p, ok := c.Provider("deepseek") |
| 151 | if !ok { |
| 152 | t.Fatal("deepseek provider missing") |
| 153 | } |
| 154 | if p.HasModel(openai.OfficialDeepSeekVisionModel) { |
| 155 | t.Fatal("explicit empty vision_models must not re-add the vision SKU to a Flash/Pro catalog") |
| 156 | } |
| 157 | if p.VisionModels == nil || len(p.VisionModels) != 0 { |
| 158 | t.Fatalf("vision_models = %#v, want preserved explicit empty list", p.VisionModels) |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | func TestDeepSeekV4PricesIncludeVisionSKU(t *testing.T) { |
| 163 | for _, currency := range []string{"CNY", "USD"} { |
| 164 | prices := DeepSeekV4PricesForCurrency(currency) |
| 165 | flash := prices["deepseek-v4-flash"] |
| 166 | got := prices[openai.OfficialDeepSeekVisionModel] |
| 167 | if flash == nil || got == nil || got.CacheHit != flash.CacheHit || got.Input != flash.Input || got.Output != flash.Output || got.Currency != flash.Currency { |
| 168 | t.Fatalf("%s vision SKU price = %+v, want Flash table %+v", currency, got, flash) |
| 169 | } |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | func TestDeepSeekOfficialPresetsRouteVisionToMultimodalSKUs(t *testing.T) { |
| 174 | for _, id := range []string{"deepseek-anthropic", "deepseek-responses"} { |
| 175 | preset, ok := CuratedProviderPreset(id) |
| 176 | if !ok || len(preset.Entries) != 1 { |
| 177 | t.Fatalf("%s preset = %+v found=%v", id, preset, ok) |
| 178 | } |
| 179 | entry := preset.Entries[0] |
| 180 | if !entry.HasModel(openai.OfficialDeepSeekVisionModel) || entry.Default != "deepseek-v4-flash" || entry.Vision { |
| 181 | t.Fatalf("%s entry = %+v", id, entry) |
| 182 | } |
| 183 | var cfg Config |
| 184 | if err := cfg.UpsertProvider(entry); err != nil { |
| 185 | t.Fatalf("UpsertProvider(%s): %v", id, err) |
| 186 | } |
| 187 | flash, _ := cfg.ResolveModel(entry.Name + "/deepseek-v4-flash") |
| 188 | pro, _ := cfg.ResolveModel(entry.Name + "/deepseek-v4-pro") |
| 189 | vision, ok := cfg.ResolveModel(entry.Name + "/" + openai.OfficialDeepSeekVisionModel) |
| 190 | if flash == nil || pro == nil || !ok { |
| 191 | t.Fatalf("%s models did not resolve", id) |
| 192 | } |
| 193 | if !EffectiveVision(flash) || EffectiveVision(pro) || !EffectiveVision(vision) { |
| 194 | t.Fatalf("%s vision routing = flash:%t pro:%t vision:%t", id, EffectiveVision(flash), EffectiveVision(pro), EffectiveVision(vision)) |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 |