返回 DeepSeek-Reasonix
reasoning_entry_test.go
根目录 / internal / config / reasoning_entry_test.go
1 package config
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "reflect"
8 "strings"
9 "testing"
10
11 "github.com/BurntSushi/toml"
12
13 "reasonix/internal/provider"
14 )
15
16 func TestLegacyReasoningDefaultsStaySeparateFromSavedOverrides(t *testing.T) {
17 path := filepath.Join(t.TempDir(), "config.toml")
18 raw := `[[providers]]
19 name = "renamed-gateway"
20 kind = "openai"
21 base_url = "https://tokenrhythm.studio/v1"
22 models = ["deepseek-flash", "glm-5.1"]
23 future_field = "preserve-me"
24 `
25 if err := os.WriteFile(path, []byte(raw), 0600); err != nil {
26 t.Fatal(err)
27 }
28 cfg := LoadForEdit(path)
29 baseline := cfg.ModelSettingsBaseline()
30 for _, tc := range []struct{ model, protocol, effort string }{
31 {"deepseek-flash", "deepseek", "high"}, {"glm-5.1", "glm", "enabled"},
32 } {
33 e, ok := cfg.ResolveModel("renamed-gateway/" + tc.model)
34 if !ok {
35 t.Fatal(tc.model)
36 }
37 if got := ReasoningProtocolForEntry(e); got != tc.protocol {
38 t.Fatalf("protocol = %q", got)
39 }
40 if err := ReasoningCapabilityForEntry(e).Validate(e.Model, tc.effort); err != nil {
41 t.Fatal(err)
42 }
43 // Catalog/settings callers that have not used ResolveModel must agree.
44 unresolved := cfg.Providers[0]
45 unresolved.Model = tc.model
46 if !reflect.DeepEqual(ResolveReasoningView(&unresolved), ResolveReasoningView(e)) {
47 t.Fatalf("UI/runtime disagree for %s", tc.model)
48 }
49 }
50 cfg.Providers[0].DisplayName = "My gateway"
51 if err := cfg.SaveModelSettingsTo(path, baseline); err != nil {
52 t.Fatal(err)
53 }
54 saved, err := os.ReadFile(path)
55 if err != nil {
56 t.Fatal(err)
57 }
58 if strings.Contains(string(saved), "supported_efforts") || strings.Contains(string(saved), "reasoning_protocol") || !strings.Contains(string(saved), "preserve-me") {
59 t.Fatalf("automatic capability leaked into saved user overrides: %s", saved)
60 }
61 // A subsequent catalog correction is visible to the same old connection.
62 entry := cfg.Providers[0]
63 entry.Model = "deepseek-flash"
64 entry.ModelOverrides = map[string]ProviderModelOverride{"deepseek-flash": {SupportedEfforts: []string{"high"}, DefaultEffort: "high"}}
65 cap := ReasoningCapabilityForEntry(&entry)
66 if !reflect.DeepEqual(cap.IDs(), []string{"high"}) || cap.Validate(entry.Model, "max") == nil {
67 t.Fatalf("explicit model vocabulary lost: %+v", cap)
68 }
69 entry.ModelOverrides = nil
70 entry.SupportedEfforts = []string{"custom"}
71 entry.DefaultEffort = "custom"
72 if cap := ReasoningCapabilityForEntry(&entry); !reflect.DeepEqual(cap.IDs(), []string{"custom"}) {
73 t.Fatal(cap)
74 }
75 }
76
77 func TestReasoningDefaultsRespectEndpointProtocolAndExactModel(t *testing.T) {
78 base := ProviderEntry{Name: "token-rhythm", Kind: "openai", BaseURL: "https://tokenrhythm.studio/v1", Model: "deepseek-flash"}
79 for _, tc := range []struct {
80 name string
81 edit func(*ProviderEntry)
82 state string
83 }{
84 {"renamed", func(e *ProviderEntry) { e.Name = "my-account" }, "supported"},
85 {"unknown model", func(e *ProviderEntry) { e.Model = "DeepSeek-Flash" }, "unknown"},
86 {"custom endpoint", func(e *ProviderEntry) { e.BaseURL = "https://relay.invalid/v1" }, "unknown"},
87 {"request override", func(e *ProviderEntry) { e.RequestURL = "https://relay.invalid/chat/completions" }, "unknown"},
88 {"disabled", func(e *ProviderEntry) { e.ReasoningProtocol = "none" }, "unsupported"},
89 } {
90 t.Run(tc.name, func(t *testing.T) {
91 e := base
92 tc.edit(&e)
93 cap := ReasoningCapabilityForEntry(&e)
94 if cap.State() != tc.state {
95 t.Fatalf("cap = %+v", cap)
96 }
97 if err := cap.Validate(e.Model, ""); err != nil {
98 t.Fatal(err)
99 }
100 if tc.state == "unknown" {
101 var diagnostic *provider.UnsupportedReasoningEffort
102 err := cap.Validate(e.Model, "high")
103 if !errors.As(err, &diagnostic) || !diagnostic.Unknown || !strings.Contains(err.Error(), "Select auto") {
104 t.Fatalf("diagnostic = %v", err)
105 }
106 }
107 })
108 }
109 base.ReasoningProtocol = "glm"
110 if cap := ReasoningCapabilityForEntry(&base); !reflect.DeepEqual(cap.IDs(), []string{"enabled", "disabled"}) {
111 t.Fatal(cap)
112 }
113 }
114
115 func TestPresetReasoningIsInheritedAndExplicitDeclarationsRoundTrip(t *testing.T) {
116 preset, _ := CuratedProviderPreset("token-rhythm")
117 e := preset.Entries[0]
118 if len(e.ModelOverrides["deepseek-flash"].SupportedEfforts) != 0 {
119 t.Fatal("preset froze generated levels")
120 }
121 path := filepath.Join(t.TempDir(), "config.toml")
122 cfg := &Config{}
123 if err := cfg.UpsertProvider(e); err != nil {
124 t.Fatal(err)
125 }
126 if err := cfg.SaveTo(path); err != nil {
127 t.Fatal(err)
128 }
129 loaded := LoadForEdit(path)
130 resolved, _ := loaded.ResolveModel("token-rhythm/deepseek-flash")
131 if err := ReasoningCapabilityForEntry(resolved).Validate("deepseek-flash", "high"); err != nil {
132 t.Fatal(err)
133 }
134 e.ModelOverrides["deepseek-flash"] = ProviderModelOverride{ReasoningProtocol: "none"}
135 if err := cfg.UpsertProvider(e); err != nil {
136 t.Fatal(err)
137 }
138 if err := cfg.SaveTo(path); err != nil {
139 t.Fatal(err)
140 }
141 resolved, _ = LoadForEdit(path).ResolveModel("token-rhythm/deepseek-flash")
142 if cap := ReasoningCapabilityForEntry(resolved); cap.State() != "unsupported" {
143 t.Fatal(cap)
144 }
145 }
146
147 func TestSessionEffortIsBoundToExactModelIdentity(t *testing.T) {
148 cfg := &Config{Providers: []ProviderEntry{{Name: "a", Models: []string{"one", "two"}, Default: "one"}, {Name: "b", Model: "one"}}}
149 for _, tc := range []struct {
150 from, to string
151 keep bool
152 }{
153 {"a/one", "a/one", true}, {"a", "a/one", true}, {"a/one", "a/two", false},
154 {"a/one", "b/one", false}, {"plugin/foo/model", "plugin/bar/model", false},
155 } {
156 value := "invalid-explicit-level"
157 got := RebindSessionEffort(cfg, tc.from, tc.to, &value)
158 if (got != nil) != tc.keep {
159 t.Fatalf("%s -> %s: %v", tc.from, tc.to, got)
160 }
161 if got != nil && (*got != value || got == &value) {
162 t.Fatal("same-route explicit intent must be independently retained")
163 }
164 }
165 }
166
167 func TestReasoningSnapshotSupportsOlderReadersAndWriters(t *testing.T) {
168 preset, _ := CuratedProviderPreset("token-rhythm")
169 e := preset.Entries[0]
170 e.ModelOverrides["deepseek-flash"] = ProviderModelOverride{DefaultEffort: "max", ContextWindow: 123456}
171 cfg := &Config{Providers: []ProviderEntry{e}}
172 body := RenderTOML(cfg)
173 // An older reader knows the original fields and ignores the optional marker.
174 type oldOverride struct {
175 ReasoningProtocol string `toml:"reasoning_protocol"`
176 SupportedEfforts []string `toml:"supported_efforts"`
177 DefaultEffort string `toml:"default_effort"`
178 ContextWindow int `toml:"context_window"`
179 }
180 var old struct {
181 Providers []struct {
182 Name string `toml:"name"`
183 Kind string `toml:"kind"`
184 BaseURL string `toml:"base_url"`
185 Models []string `toml:"models"`
186 ModelOverrides map[string]oldOverride `toml:"model_overrides"`
187 } `toml:"providers"`
188 }
189 if _, err := toml.Decode(body, &old); err != nil {
190 t.Fatal(err)
191 }
192 ov := old.Providers[0].ModelOverrides["deepseek-flash"]
193 if ov.ReasoningProtocol != "deepseek" || len(ov.SupportedEfforts) != 4 || ov.DefaultEffort != "max" {
194 t.Fatal(ov)
195 }
196 path := filepath.Join(t.TempDir(), "config.toml")
197 if err := os.WriteFile(path, []byte(body), 0600); err != nil {
198 t.Fatal(err)
199 }
200 current := LoadForEdit(path)
201 explicit := current.Providers[0].ModelOverrides["deepseek-flash"]
202 if explicit.DefaultEffort != "max" || explicit.ContextWindow != 123456 || len(explicit.SupportedEfforts) != 0 || explicit.ReasoningProtocol != "" {
203 t.Fatalf("generated fields froze or user fields lost: %+v", explicit)
204 }
205 // A legacy editor changes the vocabulary and drops the unknown marker.
206 ov.SupportedEfforts, ov.DefaultEffort = []string{"high"}, "high"
207 old.Providers[0].ModelOverrides["deepseek-flash"] = ov
208 var edited strings.Builder
209 if err := toml.NewEncoder(&edited).Encode(old); err != nil {
210 t.Fatal(err)
211 }
212 if err := os.WriteFile(path, []byte(edited.String()), 0600); err != nil {
213 t.Fatal(err)
214 }
215 resolved, _ := LoadForEdit(path).ResolveModel("token-rhythm/deepseek-flash")
216 if cap := ReasoningCapabilityForEntry(resolved); !reflect.DeepEqual(cap.IDs(), []string{"high"}) {
217 t.Fatalf("legacy edit lost: %+v", cap)
218 }
219 // Editors retaining unknown fields also retain manual reasoning changes.
220 snapshot := reasoningCompatibilitySnapshot(e).ModelOverrides["deepseek-flash"]
221 snapshot.SupportedEfforts = []string{"custom"}
222 if got := explicitModelReasoning(snapshot); !reflect.DeepEqual(got.SupportedEfforts, []string{"custom"}) || got.ReasoningProtocol != "deepseek" {
223 t.Fatalf("changed snapshot erased: %+v", got)
224 }
225 }
226
227 func TestReasoningSnapshotDoesNotShadowConnectionEditsInOlderReaders(t *testing.T) {
228 e := ProviderEntry{Name: "gateway", Kind: "openai", BaseURL: "https://tokenrhythm.studio/v1", Models: []string{"deepseek-flash"}, ReasoningProtocol: "deepseek", SupportedEfforts: []string{"high"}, DefaultEffort: "high"}
229 var previousReader Config
230 if _, err := toml.Decode(RenderTOML(&Config{Providers: []ProviderEntry{e}}), &previousReader); err != nil {
231 t.Fatal(err)
232 }
233 ov := previousReader.Providers[0].ModelOverrides["deepseek-flash"]
234 if ov.ReasoningProtocol != "" || len(ov.SupportedEfforts) != 0 || ov.DefaultEffort != "" {
235 t.Fatalf("snapshot would shadow a legacy editor's provider-level changes: %+v", ov)
236 }
237 }
238
239 func TestSavingResolvedRuntimePreservesReasoningInheritance(t *testing.T) {
240 raw := ProviderEntry{Name: "gateway", Kind: "openai", BaseURL: "https://tokenrhythm.studio/v1", Model: "deepseek-flash", DefaultEffort: "max"}
241 runtime := ResolveReasoningEntry(&raw)
242 runtime.Effort = "low"
243 cfg := &Config{}
244 if err := cfg.UpsertProvider(*runtime); err != nil {
245 t.Fatal(err)
246 }
247 saved := cfg.Providers[0]
248 if saved.ReasoningProtocol != "" || len(saved.SupportedEfforts) != 0 || saved.DefaultEffort != "max" || saved.Effort != "low" {
249 t.Fatalf("runtime defaults froze or explicit values lost: %+v", saved)
250 }
251 runtime.SupportedEfforts = []string{"low"}
252 if err := cfg.UpsertProvider(*runtime); err != nil {
253 t.Fatal(err)
254 }
255 if !reflect.DeepEqual(cfg.Providers[0].SupportedEfforts, []string{"low"}) {
256 t.Fatal("post-resolution edit lost")
257 }
258 }
259
259 lines GO