1package provider
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestSetUserModelsPreservesCatalogWithoutLiveOverlay(t *testing.T) {
11 SetLiveModels(nil)
12 t.Cleanup(func() { SetLiveModels(nil) })
13
14 SetUserModels([]Model{{
15 Provider: "custom-test",
16 ID: "custom-model",
17 DisplayName: "Custom Model",
18 Source: "user",
19 }})
20
21 if _, err := FindModel("anthropic", "claude-sonnet-4-5"); err != nil {
22 t.Fatalf("built-in model hidden after SetUserModels: %v", err)
23 }
24 if _, err := FindModel("custom-test", "custom-model"); err != nil {
25 t.Fatalf("custom model missing after SetUserModels: %v", err)
26 }
27}
28
29func TestLoadUserModelsRegistersModelLevelBaseURLCustomProvider(t *testing.T) {
30 dir := t.TempDir()
31 path := filepath.Join(dir, "models.json")
32 if err := os.WriteFile(path, []byte(`{
33 "providers": {
34 "model-base-only": {
35 "models": [
36 {"id": "m1", "baseUrl": "https://llm.example.com/v1"}
37 ]
38 }
39 }
40 }`), 0o644); err != nil {
41 t.Fatal(err)
42 }
43
44 models, warnings := LoadUserModelsWithWarnings(path)
45 if len(warnings) != 0 {
46 t.Fatalf("warnings = %v, want none", warnings)
47 }
48 if len(models) != 1 {
49 t.Fatalf("models = %d, want 1", len(models))
50 }
51 cfg, ok := CustomProviders()["model-base-only"]
52 if !ok {
53 t.Fatal("custom provider was not registered")
54 }
55 if cfg.API != "openai" {
56 t.Fatalf("api = %q, want openai", cfg.API)
57 }
58}
59
60func TestLoadUserModelsReasoningLevelMap(t *testing.T) {
61 dir := t.TempDir()
62 path := filepath.Join(dir, "models.json")
63 if err := os.WriteFile(path, []byte(`{
64 "providers": {
65 "company-proxy": {
66 "baseUrl": "https://llm.example.com/v1",
67 "api": "anthropic",
68 "models": [{
69 "id": "reasoning-model",
70 "reasoning": true,
71 "reasoningLevelMap": {"minimal": "low", "max": "off"}
72 }]
73 }
74 }
75 }`), 0o644); err != nil {
76 t.Fatal(err)
77 }
78
79 models, warnings := LoadUserModelsWithWarnings(path)
80 if len(warnings) != 0 {
81 t.Fatalf("warnings = %v, want none", warnings)
82 }
83 if len(models) != 1 {
84 t.Fatalf("models = %d, want 1", len(models))
85 }
86 model := models[0]
87 if model.API != "anthropic" {
88 t.Fatalf("api = %q, want anthropic", model.API)
89 }
90 if model.ReasoningLevelMap["minimum"] != "low" {
91 t.Fatalf("minimum mapping = %q, want low", model.ReasoningLevelMap["minimum"])
92 }
93 if mapped, ok := model.ReasoningLevelMap["max"]; !ok || mapped != "" {
94 t.Fatalf("max mapping = %q, %v, want explicit removal", mapped, ok)
95 }
96}
97
98func TestLoadUserModelsWarnsOnInvalidReasoningLevelMap(t *testing.T) {
99 dir := t.TempDir()
100 path := filepath.Join(dir, "models.json")
101 if err := os.WriteFile(path, []byte(`{
102 "providers": {
103 "custom": {
104 "models": [{
105 "id": "bad-map",
106 "reasoning": true,
107 "reasoningLevelMap": {"turbo": "high", "low": "extreme"}
108 }]
109 }
110 }
111 }`), 0o644); err != nil {
112 t.Fatal(err)
113 }
114
115 models, warnings := LoadUserModelsWithWarnings(path)
116 if len(models) != 1 || len(models[0].ReasoningLevelMap) != 0 {
117 t.Fatalf("invalid map was not discarded: %#v", models)
118 }
119 if len(warnings) != 2 || !strings.Contains(strings.Join(warnings, "\n"), "reasoningLevelMap") {
120 t.Fatalf("warnings = %v, want two reasoningLevelMap warnings", warnings)
121 }
122}
123
124func TestLoadUserModelsAcceptsOpenAIResponsesAPI(t *testing.T) {
125 dir := t.TempDir()
126 path := filepath.Join(dir, "models.json")
127 if err := os.WriteFile(path, []byte(`{
128 "providers": {
129 "company-proxy": {
130 "baseUrl": "https://llm.example.com/v1",
131 "api": "openai-responses",
132 "models": [{"id": "reasoning-model"}]
133 }
134 }
135 }`), 0o644); err != nil {
136 t.Fatal(err)
137 }
138
139 _, warnings := LoadUserModelsWithWarnings(path)
140 if len(warnings) != 0 {
141 t.Fatalf("warnings = %v, want none", warnings)
142 }
143 if cfg := CustomProviders()["company-proxy"]; cfg.API != APIResponses {
144 t.Fatalf("api = %q, want %q", cfg.API, APIResponses)
145 }
146}
147
148func TestLoadUserModelsWarnsOnUnknownAPI(t *testing.T) {
149 dir := t.TempDir()
150 path := filepath.Join(dir, "models.json")
151 if err := os.WriteFile(path, []byte(`{
152 "providers": {
153 "bad-api": {
154 "baseUrl": "https://llm.example.com/v1",
155 "api": "anthropic-message",
156 "models": [{"id": "m1"}]
157 }
158 }
159 }`), 0o644); err != nil {
160 t.Fatal(err)
161 }
162
163 _, warnings := LoadUserModelsWithWarnings(path)
164 if len(warnings) != 1 || !strings.Contains(warnings[0], "unknown api") {
165 t.Fatalf("warnings = %v, want unknown api warning", warnings)
166 }
167 if cfg := CustomProviders()["bad-api"]; cfg.API != "openai" {
168 t.Fatalf("api = %q, want openai", cfg.API)
169 }
170}