返回 DeepSeek-Reasonix
provider_endpoint_contract_test.go
根目录 / internal / config / provider_endpoint_contract_test.go
1 package config
2
3 import (
4 "os"
5 "path/filepath"
6 "reflect"
7 "strings"
8 "testing"
9 )
10
11 func TestDeepSeekEndpointContract(t *testing.T) {
12 preset, ok := CuratedProviderPreset("deepseek-anthropic")
13 if !ok {
14 t.Fatal("hidden DeepSeek preset missing")
15 }
16 catalog := CatalogForProviderPreset(preset)
17 want := map[string]string{
18 "openai": "https://api.deepseek.com/v1/chat/completions",
19 "responses": "https://api.deepseek.com/responses",
20 "anthropic": "https://api.deepseek.com/anthropic/v1/messages",
21 }
22 for kind, requestURL := range want {
23 route, exists := catalog.Protocols[kind]
24 if !exists {
25 t.Fatalf("DeepSeek %s route missing", kind)
26 }
27 if got := ProviderRequestURL(kind, route.BaseURL); got != requestURL {
28 t.Fatalf("DeepSeek %s request URL = %q, want %q", kind, got, requestURL)
29 }
30 }
31 }
32
33 func TestProviderEndpointMismatchIsConservative(t *testing.T) {
34 cases := []struct {
35 name string
36 entry ProviderEntry
37 mismatch bool
38 }{
39 {"foreign standard suffix", ProviderEntry{Kind: "openai", RequestURL: "https://gateway.test/v1/messages"}, true},
40 {"deepseek hybrid path", ProviderEntry{Name: "deepseek-anthropic", Kind: "openai", RequestURL: "https://api.deepseek.com/anthropic/v1/chat/completions"}, true},
41 {"deepseek official chat", ProviderEntry{Name: "deepseek-anthropic", Kind: "openai", RequestURL: "https://api.deepseek.com/v1/chat/completions"}, false},
42 {"mimo shared openai and responses root", ProviderEntry{Name: "mimo-api", Kind: "openai", BaseURL: "https://api.xiaomimimo.com/v1"}, false},
43 {"deepseek accepted root alias", ProviderEntry{Name: "deepseek-anthropic", Kind: "openai", RequestURL: "https://api.deepseek.com/chat/completions"}, false},
44 {"custom host", ProviderEntry{Name: "deepseek-anthropic", Kind: "openai", RequestURL: "https://gateway.test/anthropic/v1/chat/completions"}, false},
45 {"query override", ProviderEntry{Name: "deepseek-anthropic", Kind: "openai", RequestURL: "https://api.deepseek.com/anthropic/v1/chat/completions?token=x"}, false},
46 {"unknown path", ProviderEntry{Name: "deepseek-anthropic", Kind: "openai", RequestURL: "https://api.deepseek.com/custom/route"}, false},
47 {"custom path in foreign namespace", ProviderEntry{Name: "deepseek-anthropic", Kind: "openai", RequestURL: "https://api.deepseek.com/anthropic/custom/chat/completions"}, false},
48 }
49 for _, tc := range cases {
50 t.Run(tc.name, func(t *testing.T) {
51 got := ProviderEndpointMismatchForEntry(&tc.entry)
52 if (got != nil) != tc.mismatch {
53 t.Fatalf("mismatch = %+v, want %v", got, tc.mismatch)
54 }
55 if got != nil && tc.entry.Name == "deepseek-anthropic" && got.Recommended != "https://api.deepseek.com/v1/chat/completions" {
56 t.Fatalf("recommendation = %q", got.Recommended)
57 }
58 })
59 }
60 }
61
62 func TestCatalogForProviderEntryUsesHiddenLegacyPreset(t *testing.T) {
63 id, catalog, ok := CatalogForProviderEntry(&ProviderEntry{Name: "deepseek-anthropic"})
64 if !ok || id != "deepseek-anthropic" || catalog.BrandID != "deepseek" || catalog.Region != "global" || catalog.Product != "api" {
65 t.Fatalf("catalog identity = %q %+v %v", id, catalog, ok)
66 }
67 }
68
69 func TestRepairProviderEndpointContractUsesExactCatalogRoute(t *testing.T) {
70 legacyStateful := true
71 entry := ProviderEntry{
72 Name: "deepseek-anthropic", DisplayName: "Deepseek2", PresetID: "deepseek-anthropic",
73 Kind: "responses", BaseURL: "https://api.deepseek.com", RequestURL: "https://api.deepseek.com/anthropic/v1/messages",
74 ChatURL: "https://stale.example/chat/completions", Models: []string{"deepseek-v4-flash"}, Default: "deepseek-v4-flash",
75 APIKeyEnv: "MY_DEEPSEEK_KEY", Headers: map[string]string{"X-Test": "keep"}, ExtraBody: map[string]any{"future": "keep"},
76 ContextWindow: 1_000_000, ResponsesMode: "stateful", ResponsesStateful: &legacyStateful,
77 }
78 repair, changed := RepairProviderEndpointContract(&entry)
79 if !changed || repair == nil {
80 t.Fatal("expected exact DeepSeek route to be repaired")
81 }
82 if repair.ProviderName != "Deepseek2" || repair.FromProtocol != "responses" || repair.ToProtocol != "anthropic" ||
83 repair.RequestURL != "https://api.deepseek.com/anthropic/v1/messages" {
84 t.Fatalf("repair = %+v", repair)
85 }
86 if entry.Kind != "anthropic" || entry.BaseURL != "https://api.deepseek.com/anthropic" ||
87 entry.RequestURL != "" || entry.ChatURL != "" || entry.ResponsesMode != "" || entry.ResponsesStateful != nil {
88 t.Fatalf("repaired entry = %+v", entry)
89 }
90 if got := ProviderEffectiveRequestURL(&entry); got != "https://api.deepseek.com/anthropic/v1/messages" {
91 t.Fatalf("effective request URL = %q", got)
92 }
93 if entry.Name != "deepseek-anthropic" || entry.PresetID != "deepseek-anthropic" ||
94 entry.APIKeyEnv != "MY_DEEPSEEK_KEY" || entry.ContextWindow != 1_000_000 ||
95 !reflect.DeepEqual(entry.Models, []string{"deepseek-v4-flash"}) ||
96 !reflect.DeepEqual(entry.Headers, map[string]string{"X-Test": "keep"}) ||
97 !reflect.DeepEqual(entry.ExtraBody, map[string]any{"future": "keep"}) {
98 t.Fatalf("repair discarded user-owned fields: %+v", entry)
99 }
100 if second, changed := RepairProviderEndpointContract(&entry); changed || second != nil {
101 t.Fatalf("repair is not idempotent: %+v changed=%v", second, changed)
102 }
103 }
104
105 func TestRepairProviderEndpointContractAppliesTargetRouteOptions(t *testing.T) {
106 tests := []struct {
107 name string
108 entry ProviderEntry
109 wantKind string
110 wantBaseURL string
111 wantMode string
112 wantAuth bool
113 wantRequest string
114 }{
115 {
116 name: "responses",
117 entry: ProviderEntry{Name: "deepseek-anthropic", Kind: "anthropic", BaseURL: "https://api.deepseek.com/anthropic",
118 RequestURL: "https://api.deepseek.com/responses", AuthHeader: true},
119 wantKind: "responses", wantBaseURL: "https://api.deepseek.com", wantMode: "stateless",
120 wantRequest: "https://api.deepseek.com/responses",
121 },
122 {
123 name: "openai",
124 entry: ProviderEntry{Name: "deepseek-anthropic", Kind: "anthropic", BaseURL: "https://api.deepseek.com/anthropic",
125 RequestURL: "https://api.deepseek.com/v1/chat/completions", AuthHeader: true, ResponsesMode: "stateful"},
126 wantKind: "openai", wantBaseURL: "https://api.deepseek.com/v1",
127 wantRequest: "https://api.deepseek.com/v1/chat/completions",
128 },
129 }
130 for _, tc := range tests {
131 t.Run(tc.name, func(t *testing.T) {
132 if _, changed := RepairProviderEndpointContract(&tc.entry); !changed {
133 t.Fatal("expected repair")
134 }
135 if tc.entry.Kind != tc.wantKind || tc.entry.BaseURL != tc.wantBaseURL ||
136 tc.entry.ResponsesMode != tc.wantMode || tc.entry.AuthHeader != tc.wantAuth ||
137 ProviderEffectiveRequestURL(&tc.entry) != tc.wantRequest {
138 t.Fatalf("entry = %+v, request = %q", tc.entry, ProviderEffectiveRequestURL(&tc.entry))
139 }
140 })
141 }
142 }
143
144 func TestRepairProviderEndpointContractPreservesUnprovenRoutes(t *testing.T) {
145 tests := []ProviderEntry{
146 {Name: "deepseek-anthropic", Kind: "responses", RequestURL: "https://gateway.test/v1/messages"},
147 {Name: "deepseek-anthropic", Kind: "responses", RequestURL: "https://api.deepseek.com/anthropic/v1/messages?route=custom"},
148 {Name: "deepseek-anthropic", Kind: "responses", RequestURL: "https://user@api.deepseek.com/anthropic/v1/messages"},
149 {Name: "deepseek-anthropic", Kind: "responses", RequestURL: "https://api.deepseek.com/custom/messages"},
150 {Name: "custom", Kind: "responses", RequestURL: "https://api.deepseek.com/anthropic/v1/messages"},
151 }
152 for _, entry := range tests {
153 before := entry
154 if repair, changed := RepairProviderEndpointContract(&entry); changed || repair != nil {
155 t.Fatalf("unexpected repair for %+v: %+v", before, repair)
156 }
157 if !reflect.DeepEqual(entry, before) {
158 t.Fatalf("unproven route changed: before=%+v after=%+v", before, entry)
159 }
160 }
161 }
162
163 func TestProjectProviderEndpointRepairIsRuntimeOnly(t *testing.T) {
164 t.Setenv("REASONIX_HOME", t.TempDir())
165 root := t.TempDir()
166 path := filepath.Join(root, "reasonix.toml")
167 raw := `[[providers]]
168 name = "deepseek-anthropic"
169 preset_id = "deepseek-anthropic"
170 kind = "responses"
171 base_url = "https://api.deepseek.com"
172 request_url = "https://api.deepseek.com/anthropic/v1/messages"
173 models = ["deepseek-v4-flash"]
174 default = "deepseek-v4-flash"
175 `
176 if err := os.WriteFile(path, []byte(raw), 0o644); err != nil {
177 t.Fatal(err)
178 }
179 cfg, err := LoadForRootWithoutCredentialsReadOnly(root)
180 if err != nil {
181 t.Fatal(err)
182 }
183 p, ok := cfg.Provider("deepseek-anthropic")
184 if !ok || p.Kind != "anthropic" || p.BaseURL != "https://api.deepseek.com/anthropic" ||
185 ProviderEffectiveRequestURL(p) != "https://api.deepseek.com/anthropic/v1/messages" {
186 t.Fatalf("runtime provider = %+v found=%v", p, ok)
187 }
188 after, err := os.ReadFile(path)
189 if err != nil {
190 t.Fatal(err)
191 }
192 if string(after) != raw {
193 t.Fatalf("project config was rewritten:\n%s", after)
194 }
195 }
196
197 func TestRewriteProviderEndpointContractsPreservesInlineUnknownFields(t *testing.T) {
198 raw := `config_version = 10 # keep
199 providers = [{name="deepseek-anthropic", preset_id="deepseek-anthropic", kind="anthropic", base_url="https://api.deepseek.com/anthropic", request_url="https://api.deepseek.com/responses", responses_stateful=true, api_key_env="MY_KEY", models=["deepseek-v4-flash"], future={value="keep"}}] # keep inline
200 `
201 next, repairs, err := rewriteProviderEndpointContracts(raw)
202 if err != nil {
203 t.Fatal(err)
204 }
205 if len(repairs) != 1 || repairs[0].FromProtocol != "anthropic" || repairs[0].ToProtocol != "responses" {
206 t.Fatalf("repairs = %+v", repairs)
207 }
208 for _, want := range []string{
209 "config_version = 10 # keep",
210 `kind="responses"`,
211 `base_url="https://api.deepseek.com"`,
212 `request_url=""`,
213 `responses_mode = "stateless"`,
214 `api_key_env="MY_KEY"`,
215 `future={value="keep"}`,
216 "# keep inline",
217 } {
218 if !strings.Contains(next, want) {
219 t.Fatalf("rewritten inline config missing %q:\n%s", want, next)
220 }
221 }
222 if strings.Contains(next, "responses_stateful") {
223 t.Fatalf("legacy responses state survived:\n%s", next)
224 }
225 path := filepath.Join(t.TempDir(), "config.toml")
226 if err := os.WriteFile(path, []byte(next), 0o600); err != nil {
227 t.Fatal(err)
228 }
229 loaded := LoadForEdit(path)
230 p, ok := loaded.Provider("deepseek-anthropic")
231 if !ok || p.Kind != "responses" || p.ResponsesMode != "stateless" ||
232 ProviderEffectiveRequestURL(p) != "https://api.deepseek.com/responses" {
233 t.Fatalf("reloaded inline provider = %+v found=%v", p, ok)
234 }
235 }
236
236 lines GO