返回 DeepSeek-Reasonix
stepfun_test.go
根目录 / internal / provider / openai / stepfun_test.go
1 package openai
2
3 import (
4 "strings"
5 "testing"
6
7 "reasonix/internal/provider"
8 )
9
10 const (
11 wantStepFunComChat = "https://api.stepfun.com/step_plan/v1/chat/completions"
12 wantStepFunComModels = "https://api.stepfun.com/step_plan/v1/models"
13 wantStepFunAIChat = "https://api.stepfun.ai/step_plan/v1/chat/completions"
14 wantStepFunAIModels = "https://api.stepfun.ai/step_plan/v1/models"
15 )
16
17 func TestCanonicalStepFunPlanEndpoints(t *testing.T) {
18 t.Parallel()
19
20 tests := []struct {
21 name string
22 raw string
23 ok bool
24 wantChat string
25 wantModels string
26 }{
27 {name: "com v1 base", raw: "https://api.stepfun.com/step_plan/v1", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
28 {name: "com missing v1 base", raw: "https://api.stepfun.com/step_plan", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
29 {name: "com missing v1 trailing slash", raw: "https://api.stepfun.com/step_plan/", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
30 {name: "com missing v1 chat", raw: "https://api.stepfun.com/step_plan/chat/completions", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
31 {name: "com missing v1 models", raw: "https://api.stepfun.com/step_plan/models", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
32 {name: "com v1 chat", raw: "https://api.stepfun.com/step_plan/v1/chat/completions", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
33 {name: "com v1 models", raw: "https://api.stepfun.com/step_plan/v1/models", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
34 {name: "com double v1 base", raw: "https://api.stepfun.com/step_plan/v1/v1", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
35 {name: "com double v1 chat", raw: "https://api.stepfun.com/step_plan/v1/v1/chat/completions", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
36 {name: "com repeated chat suffix", raw: "https://api.stepfun.com/step_plan/v1/chat/completions/chat/completions", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
37 {name: "ai v1 base", raw: "https://api.stepfun.ai/step_plan/v1", ok: true, wantChat: wantStepFunAIChat, wantModels: wantStepFunAIModels},
38 {name: "ai missing v1 base", raw: "https://api.stepfun.ai/step_plan", ok: true, wantChat: wantStepFunAIChat, wantModels: wantStepFunAIModels},
39 {name: "ai missing v1 chat", raw: "https://api.stepfun.ai/step_plan/chat/completions", ok: true, wantChat: wantStepFunAIChat, wantModels: wantStepFunAIModels},
40 {name: "uppercase hostname", raw: "https://API.STEPFUN.COM/step_plan/v1", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
41 {name: "explicit 443", raw: "https://api.stepfun.com:443/step_plan/v1", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
42 {name: "whitespace around official url", raw: " https://api.stepfun.com/step_plan/v1 ", ok: true, wantChat: wantStepFunComChat, wantModels: wantStepFunComModels},
43
44 {name: "stepfun apex domain", raw: "https://stepfun.com/step_plan/v1", ok: false},
45 {name: "subdomain of stepfun.com", raw: "https://gateway.stepfun.com/step_plan/v1", ok: false},
46 {name: "lookalike domain", raw: "https://api.stepfun.com.example.com/step_plan/v1", ok: false},
47 {name: "third-party gateway", raw: "https://relay.example.com/step_plan/v1", ok: false},
48 {name: "standard api root keeps openai shape", raw: "https://api.stepfun.com/v1", ok: false},
49 {name: "standard api chat keeps openai shape", raw: "https://api.stepfun.com/v1/chat/completions", ok: false},
50 {name: "bare host", raw: "https://api.stepfun.com", ok: false},
51 {name: "http scheme", raw: "http://api.stepfun.com/step_plan/v1", ok: false},
52 {name: "non-443 port", raw: "https://api.stepfun.com:8443/step_plan/v1", ok: false},
53 {name: "userinfo", raw: "https://user@api.stepfun.com/step_plan/v1", ok: false},
54 {name: "query", raw: "https://api.stepfun.com/step_plan/v1?foo=1", ok: false},
55 {name: "fragment", raw: "https://api.stepfun.com/step_plan/v1#frag", ok: false},
56 {name: "escaped path slash", raw: "https://api.stepfun.com/step_plan%2Fv1", ok: false},
57 {name: "unknown path under step_plan", raw: "https://api.stepfun.com/step_plan/openai", ok: false},
58 {name: "anthropic messages path", raw: "https://api.stepfun.com/step_plan/v1/messages", ok: false},
59 {name: "triple v1", raw: "https://api.stepfun.com/step_plan/v1/v1/v1", ok: false},
60 {name: "empty", raw: "", ok: false},
61 {name: "garbage", raw: "not a url", ok: false},
62 }
63
64 for _, tt := range tests {
65 t.Run(tt.name, func(t *testing.T) {
66 t.Parallel()
67 gotChat, chatOK := canonicalStepFunPlanChatURL(tt.raw)
68 gotModels, modelsOK := CanonicalStepFunPlanModelsURL(tt.raw)
69 if chatOK != tt.ok || modelsOK != tt.ok {
70 t.Fatalf("ok chat=%v models=%v, want %v", chatOK, modelsOK, tt.ok)
71 }
72 if !tt.ok {
73 if gotChat != "" || gotModels != "" {
74 t.Fatalf("rewrote unknown input %q to chat=%q models=%q", tt.raw, gotChat, gotModels)
75 }
76 return
77 }
78 if gotChat != tt.wantChat {
79 t.Fatalf("chat = %q, want %q", gotChat, tt.wantChat)
80 }
81 if gotModels != tt.wantModels {
82 t.Fatalf("models = %q, want %q", gotModels, tt.wantModels)
83 }
84 if strings.Count(gotChat, "/v1") != 1 || strings.Count(gotChat, "/chat/completions") != 1 {
85 t.Fatalf("chat %q is not a single canonical endpoint", gotChat)
86 }
87 })
88 }
89 }
90
91 func TestNewCanonicalizesStepFunPlanChatURLs(t *testing.T) {
92 t.Parallel()
93
94 tests := []struct {
95 name string
96 cfg provider.Config
97 want string
98 }{
99 {
100 name: "anthropic-docs base repaired on com host",
101 cfg: provider.Config{
102 Name: "stepfun",
103 BaseURL: "https://api.stepfun.com/step_plan",
104 Model: "step-3.7-flash",
105 },
106 want: wantStepFunComChat,
107 },
108 {
109 name: "anthropic-docs base repaired on ai host",
110 cfg: provider.Config{
111 Name: "stepfun-en",
112 BaseURL: "https://api.stepfun.ai/step_plan",
113 Model: "step-3.7-flash",
114 },
115 want: wantStepFunAIChat,
116 },
117 {
118 name: "correct v1 base unchanged",
119 cfg: provider.Config{
120 Name: "stepfun",
121 BaseURL: "https://api.stepfun.com/step_plan/v1",
122 Model: "step-3.7-flash",
123 },
124 want: wantStepFunComChat,
125 },
126 {
127 name: "request_url stepfun typo is repaired",
128 cfg: provider.Config{
129 Name: "custom-gateway",
130 BaseURL: "https://example.invalid/v1",
131 Model: "step-3.7-flash",
132 Extra: map[string]any{
133 "request_url": "https://api.stepfun.com/step_plan/v1/v1/chat/completions",
134 },
135 },
136 want: wantStepFunComChat,
137 },
138 {
139 name: "legacy chat_url stepfun typo is repaired",
140 cfg: provider.Config{
141 Name: "stepfun",
142 BaseURL: "https://example.invalid/v1",
143 Model: "step-3.7-flash",
144 Extra: map[string]any{
145 "chat_url": "https://api.stepfun.com/step_plan/chat/completions/",
146 },
147 },
148 want: wantStepFunComChat,
149 },
150 {
151 name: "third-party base_url still appends chat completions",
152 cfg: provider.Config{
153 Name: "other",
154 BaseURL: "https://base.example.com/v1",
155 Model: "test-model",
156 },
157 want: "https://base.example.com/v1/chat/completions",
158 },
159 {
160 name: "standard stepfun api root keeps openai shape",
161 cfg: provider.Config{
162 Name: "stepfun-standard",
163 BaseURL: "https://api.stepfun.com/v1",
164 Model: "step-3.7-flash",
165 },
166 want: "https://api.stepfun.com/v1/chat/completions",
167 },
168 {
169 name: "unknown official path is not guessed",
170 cfg: provider.Config{
171 Name: "stepfun",
172 BaseURL: "https://api.stepfun.com/step_plan/openai",
173 Model: "step-3.7-flash",
174 },
175 want: "https://api.stepfun.com/step_plan/openai/chat/completions",
176 },
177 {
178 name: "third-party request_url wins untouched",
179 cfg: provider.Config{
180 Name: "other",
181 BaseURL: "https://api.stepfun.com/step_plan",
182 Model: "test-model",
183 Extra: map[string]any{
184 "request_url": "https://exact.example.com/custom/",
185 },
186 },
187 want: "https://exact.example.com/custom/",
188 },
189 }
190
191 for _, tt := range tests {
192 t.Run(tt.name, func(t *testing.T) {
193 t.Parallel()
194 p, err := New(tt.cfg)
195 if err != nil {
196 t.Fatalf("New: %v", err)
197 }
198 got := p.(*client).chatURL
199 if got != tt.want {
200 t.Fatalf("chatURL = %q, want %q", got, tt.want)
201 }
202 })
203 }
204 }
205
206 // The wizard's model probe and the chat client must agree on the endpoint, so
207 // an Anthropic-docs base can never pass setup while chat 404s.
208 func TestStepFunPlanProbeAndChatShareCanonicalURL(t *testing.T) {
209 t.Parallel()
210
211 for _, base := range []string{
212 "https://api.stepfun.com/step_plan",
213 "https://api.stepfun.com/step_plan/",
214 "https://api.stepfun.com/step_plan/v1",
215 "https://api.stepfun.ai/step_plan",
216 } {
217 chatURL, chatOK := canonicalStepFunPlanChatURL(base)
218 modelsURL, modelsOK := CanonicalStepFunPlanModelsURL(base)
219 if !chatOK || !modelsOK {
220 t.Fatalf("base %q not recognized: chat=%v models=%v", base, chatOK, modelsOK)
221 }
222 wantModels := strings.Replace(chatURL, "/chat/completions", "/models", 1)
223 if modelsURL != wantModels {
224 t.Fatalf("base %q: models %q does not share chat root %q", base, modelsURL, chatURL)
225 }
226 }
227 }
228
228 lines GO