返回 DeepSeek-Reasonix
model_capabilities_v2_test.go
根目录 / internal / config / model_capabilities_v2_test.go
1 package config
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "reasonix/internal/provider"
8 "reflect"
9 "runtime"
10 "sync"
11 "testing"
12 "time"
13 )
14
15 func TestCapabilityOverrideDirectCatalogResolution(t *testing.T) {
16 r := &ModelCapabilityResolver{entries: map[string]ModelCapabilityCacheEntry{}}
17 e := ProviderEntry{Name: "opencode-go", Kind: "openai", BaseURL: "https://opencode.ai/zen/go/v1", Model: "kimi-k3"}
18 auto := r.Resolve(&e)
19 e.ModelOverrides = map[string]ProviderModelOverride{"KIMI-K3": {Vision: capabilityBoolPtr(false), ContextWindow: 123456}}
20 if got := r.Resolve(&e); got.Source == CapabilitySourceOverride {
21 t.Fatal("differently cased override must not apply")
22 }
23 e.ModelOverrides["kimi-k3"] = e.ModelOverrides["KIMI-K3"]
24 got := r.Resolve(&e)
25 if got.State != CapabilityUnsupported || got.AutomaticState != CapabilitySupported || got.Source != CapabilitySourceOverride {
26 t.Fatalf("override = %+v", got)
27 }
28 facts := got.ModelInfo
29 facts.InputModalities = auto.ModelInfo.InputModalities
30 if !reflect.DeepEqual(facts, auto.ModelInfo) {
31 t.Fatalf("override erased catalog facts: %+v vs %+v", got, auto)
32 }
33 if e.Model != "kimi-k3" || e.visionOverride != nil {
34 t.Fatal("shared entry was mutated")
35 }
36 e.Model = "uncatalogued"
37 if got := r.Resolve(&e); got.State != CapabilityUnknown {
38 t.Fatalf("other model inherited override: %+v", got)
39 }
40 e.ModelOverrides["uncatalogued"] = ProviderModelOverride{Vision: capabilityBoolPtr(true)}
41 if got := r.Resolve(&e); got.State != CapabilitySupported || got.AutomaticState != CapabilityUnknown {
42 t.Fatalf("manual enable: %+v", got)
43 }
44 delete(e.ModelOverrides, "uncatalogued")
45 if got := r.Resolve(&e); got.State != CapabilityUnknown {
46 t.Fatalf("auto: %+v", got)
47 }
48 }
49
50 func TestCapabilityOfficialHardLimitAndExplicitOff(t *testing.T) {
51 r := &ModelCapabilityResolver{}
52 for _, kind := range []string{"openai", "anthropic", "responses"} {
53 e := ProviderEntry{Name: "deepseek", Kind: kind, BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-pro", Vision: true, ModelOverrides: map[string]ProviderModelOverride{"deepseek-v4-pro": {Vision: capabilityBoolPtr(true)}}}
54 if got := r.Resolve(&e); got.State != CapabilityUnsupported || got.ImageInputEnableAllowed || got.ImageInputBlockReason == "" {
55 t.Fatalf("%s hard limit: %+v", kind, got)
56 }
57 e.BaseURL, e.RequestURL = "https://relay.test", "https://api.deepseek.com/v1/messages"
58 if got := r.Resolve(&e); got.State != CapabilityUnsupported || got.ImageInputEnableAllowed {
59 t.Fatalf("%s exact request URL bypassed hard limit: %+v", kind, got)
60 }
61 e.Model = "deepseek-v4-flash-vision-exp"
62 e.ModelOverrides[e.Model] = ProviderModelOverride{Vision: capabilityBoolPtr(false)}
63 if got := r.Resolve(&e); got.State != CapabilityUnsupported || !got.ImageInputEnableAllowed || got.Source != CapabilitySourceOverride {
64 t.Fatalf("%s vision off: %+v", kind, got)
65 }
66 }
67 }
68
69 func TestCapabilityV2IgnoresV1AndPersistsUnknown(t *testing.T) {
70 t.Setenv("REASONIX_CACHE_HOME", t.TempDir())
71 v1 := filepath.Join(CacheDir(), "model-capabilities-v1.json")
72 old := []byte(`{"version":1,"entries":[]}`)
73 if err := os.WriteFile(v1, old, 0600); err != nil {
74 t.Fatal(err)
75 }
76 e := ProviderEntry{Name: "relay", Kind: "openai", BaseURL: "https://relay.test", Model: "x"}
77 r := NewModelCapabilityResolver()
78 now := time.Now()
79 r.PutCatalogAt(e, []provider.ModelInfo{{ID: "x", InputModalities: []provider.ModelModality{provider.ModalityText, provider.ModalityImage}}}, now)
80 r.PutCatalogAt(e, []provider.ModelInfo{{ID: "x"}}, now.Add(time.Second))
81 if got := NewModelCapabilityResolver().Resolve(&e); got.State != CapabilityUnknown || got.InputModalities != nil {
82 t.Fatalf("unknown roundtrip: %+v", got)
83 }
84 if data, _ := os.ReadFile(v1); string(data) != string(old) {
85 t.Fatal("v1 was modified")
86 }
87 info, err := os.Stat(r.path)
88 if err != nil {
89 t.Fatal(err)
90 } else if runtime.GOOS != "windows" && info.Mode().Perm() != 0600 {
91 t.Fatalf("cache permissions: %o, want 600", info.Mode().Perm())
92 }
93 for _, content := range []string{"broken", `{"version":999}`, `{"version":1}`} {
94 if err := os.WriteFile(r.path, []byte(content), 0600); err != nil {
95 t.Fatal(err)
96 }
97 if got := NewModelCapabilityResolver().Resolve(&e); got.State != CapabilityUnknown {
98 t.Fatalf("invalid cache: %+v", got)
99 }
100 }
101 }
102
103 func TestCapabilityCacheNewestSuccessWinsAcrossResolvers(t *testing.T) {
104 t.Setenv("REASONIX_CACHE_HOME", t.TempDir())
105 e := ProviderEntry{Name: "relay", Kind: "openai", BaseURL: "https://relay.test", Model: "x"}
106 old, newer := NewModelCapabilityResolver(), NewModelCapabilityResolver()
107 now := time.Now()
108 newer.PutCatalogAt(e, []provider.ModelInfo{{ID: "x"}}, now.Add(time.Second))
109 old.PutCatalogAt(e, []provider.ModelInfo{{ID: "x", InputModalities: []provider.ModelModality{provider.ModalityImage}}}, now)
110 if got := old.Resolve(&e); got.State != CapabilityUnknown {
111 t.Fatalf("late response resurrected images: %+v", got)
112 }
113 if got := NewModelCapabilityResolver().Resolve(&e); got.State != CapabilityUnknown {
114 t.Fatalf("disk lost newer success: %+v", got)
115 }
116 // Concurrent cache reads and writes share no mutable Provider state.
117 var wg sync.WaitGroup
118 for range 8 {
119 wg.Go(func() {
120 old.PutCatalogAt(e, []provider.ModelInfo{{ID: "x"}}, now)
121 _ = old.Resolve(&e)
122 })
123 }
124 wg.Wait()
125 }
126
127 func TestCapabilityCacheRouteIdentity(t *testing.T) {
128 r := &ModelCapabilityResolver{}
129 e := ProviderEntry{Name: "relay", Kind: "openai", BaseURL: "https://relay.test", Model: "x"}
130 for _, mutate := range []func(*ProviderEntry){func(p *ProviderEntry) { p.NoProxy = true }, func(p *ProviderEntry) { p.ChatURL = "https://a.test/chat" }, func(p *ProviderEntry) { p.RequestURL = "https://b.test/responses" }} {
131 other := e
132 mutate(&other)
133 if r.providerFingerprint(e) == r.providerFingerprint(other) {
134 t.Fatal("route omitted from cache identity")
135 }
136 }
137 }
138
139 func TestCapabilityCacheMergeValidatesDiskModalities(t *testing.T) {
140 t.Setenv("REASONIX_CACHE_HOME", t.TempDir())
141 r := NewModelCapabilityResolver()
142 e := ProviderEntry{Name: "relay", Kind: "openai", BaseURL: "https://relay.test", Model: "malformed"}
143 file := ModelCapabilityCacheFile{Version: 2, Entries: []ModelCapabilityCacheEntry{{ProviderFingerprint: r.providerFingerprint(e), ModelID: e.Model, InputModalities: []provider.ModelModality{provider.ModalityImage, "invalid"}, FetchedAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)}}}
144 data, err := json.Marshal(file)
145 if err != nil {
146 t.Fatal(err)
147 }
148 if err := os.WriteFile(r.path, data, 0600); err != nil {
149 t.Fatal(err)
150 }
151 r.PutCatalog(e, []provider.ModelInfo{{ID: "other"}})
152 if got := r.Resolve(&e); got.State != CapabilityUnknown {
153 t.Fatalf("merge trusted invalid disk metadata: %+v", got)
154 }
155 }
156
156 lines GO