返回 DeepSeek-Reasonix
tokenrhythm_test.go
根目录 / internal / provider / openai / tokenrhythm_test.go
1 package openai
2
3 import (
4 "strings"
5 "testing"
6
7 "reasonix/internal/provider"
8 )
9
10 const (
11 wantTokenRhythmChat = "https://tokenrhythm.studio/v1/chat/completions"
12 wantTokenRhythmModels = "https://tokenrhythm.studio/v1/models"
13 )
14
15 func TestCanonicalTokenRhythmEndpoints(t *testing.T) {
16 t.Parallel()
17
18 tests := []struct {
19 name string
20 raw string
21 ok bool
22 }{
23 {name: "v1 base", raw: "https://tokenrhythm.studio/v1", ok: true},
24 {name: "v1 chat", raw: "https://tokenrhythm.studio/v1/chat/completions", ok: true},
25 {name: "v1 models", raw: "https://tokenrhythm.studio/v1/models", ok: true},
26 {name: "missing v1 root", raw: "https://tokenrhythm.studio", ok: true},
27 {name: "missing v1 slash root", raw: "https://tokenrhythm.studio/", ok: true},
28 {name: "missing v1 chat", raw: "https://tokenrhythm.studio/chat/completions", ok: true},
29 {name: "missing v1 models", raw: "https://tokenrhythm.studio/models", ok: true},
30 {name: "double v1 base", raw: "https://tokenrhythm.studio/v1/v1", ok: true},
31 {name: "double v1 chat", raw: "https://tokenrhythm.studio/v1/v1/chat/completions", ok: true},
32 {name: "double v1 models", raw: "https://tokenrhythm.studio/v1/v1/models", ok: true},
33 {name: "repeated chat suffix", raw: "https://tokenrhythm.studio/v1/chat/completions/chat/completions", ok: true},
34 {name: "repeated chat suffix without v1", raw: "https://tokenrhythm.studio/chat/completions/chat/completions", ok: true},
35 {name: "repeated chat suffix with double v1", raw: "https://tokenrhythm.studio/v1/v1/chat/completions/chat/completions", ok: true},
36 {name: "repeated models suffix", raw: "https://tokenrhythm.studio/v1/models/models", ok: true},
37 {name: "repeated models suffix without v1", raw: "https://tokenrhythm.studio/models/models", ok: true},
38 {name: "repeated models suffix with double v1", raw: "https://tokenrhythm.studio/v1/v1/models/models", ok: true},
39 {name: "uppercase hostname", raw: "https://TOKENRHYTHM.STUDIO/v1", ok: true},
40 {name: "mixed-case hostname chat", raw: "https://TokenRhythm.Studio/v1/chat/completions", ok: true},
41 {name: "explicit 443", raw: "https://tokenrhythm.studio:443/v1", ok: true},
42 {name: "explicit 443 models", raw: "https://tokenrhythm.studio:443/v1/models", ok: true},
43 {name: "trailing slash base", raw: "https://tokenrhythm.studio/v1/", ok: true},
44 {name: "trailing slash chat", raw: "https://tokenrhythm.studio/v1/chat/completions/", ok: true},
45 {name: "trailing slash models", raw: "https://tokenrhythm.studio/v1/models/", ok: true},
46 {name: "whitespace around official url", raw: " https://tokenrhythm.studio/v1 ", ok: true},
47
48 {name: "api subdomain", raw: "https://api.tokenrhythm.studio/v1", ok: false},
49 {name: "suffix domain", raw: "https://tokenrhythm.studio.example.com/v1", ok: false},
50 {name: "third-party gateway", raw: "https://gateway.example.com/v1", ok: false},
51 {name: "http scheme", raw: "http://tokenrhythm.studio/v1", ok: false},
52 {name: "non-443 port", raw: "https://tokenrhythm.studio:8443/v1", ok: false},
53 {name: "userinfo", raw: "https://user@tokenrhythm.studio/v1", ok: false},
54 {name: "userinfo password", raw: "https://user:pass@tokenrhythm.studio/v1", ok: false},
55 {name: "query", raw: "https://tokenrhythm.studio/v1?foo=1", ok: false},
56 {name: "empty query", raw: "https://tokenrhythm.studio/v1?", ok: false},
57 {name: "fragment", raw: "https://tokenrhythm.studio/v1#frag", ok: false},
58 {name: "empty fragment", raw: "https://tokenrhythm.studio/v1#", ok: false},
59 {name: "escaped path slash", raw: "https://tokenrhythm.studio/v1%2Fchat%2Fcompletions", ok: false},
60 {name: "escaped v1", raw: "https://tokenrhythm.studio/%76%31", ok: false},
61 {name: "invalid url", raw: "://tokenrhythm.studio/v1", ok: false},
62 {name: "relative url", raw: "/v1/chat/completions", ok: false},
63 {name: "missing scheme", raw: "tokenrhythm.studio/v1", ok: false},
64 {name: "unknown path", raw: "https://tokenrhythm.studio/openai", ok: false},
65 {name: "triple v1", raw: "https://tokenrhythm.studio/v1/v1/v1", ok: false},
66 {name: "chat suffix repeated twice", raw: "https://tokenrhythm.studio/v1/chat/completions/chat/completions/chat/completions", ok: false},
67 {name: "anthropic messages", raw: "https://tokenrhythm.studio/v1/messages", ok: false},
68 {name: "embeddings", raw: "https://tokenrhythm.studio/v1/embeddings", ok: false},
69 {name: "trailing-dot hostname", raw: "https://tokenrhythm.studio./v1", ok: false},
70 {name: "empty", raw: "", ok: false},
71 {name: "garbage", raw: "not a url", ok: false},
72 }
73
74 for _, tt := range tests {
75 t.Run(tt.name, func(t *testing.T) {
76 t.Parallel()
77 gotChat, chatOK := canonicalTokenRhythmChatURL(tt.raw)
78 gotModels, modelsOK := CanonicalTokenRhythmModelsURL(tt.raw)
79 if chatOK != tt.ok || modelsOK != tt.ok {
80 t.Fatalf("ok chat=%v models=%v, want %v", chatOK, modelsOK, tt.ok)
81 }
82 if !tt.ok {
83 if gotChat != "" || gotModels != "" {
84 t.Fatalf("rewrote unknown input %q to chat=%q models=%q", tt.raw, gotChat, gotModels)
85 }
86 return
87 }
88 if gotChat != wantTokenRhythmChat {
89 t.Fatalf("chat = %q, want %q", gotChat, wantTokenRhythmChat)
90 }
91 if gotModels != wantTokenRhythmModels {
92 t.Fatalf("models = %q, want %q", gotModels, wantTokenRhythmModels)
93 }
94 if strings.Count(gotChat, "/v1") != 1 || strings.Count(gotChat, "/chat/completions") != 1 {
95 t.Fatalf("chat %q is not a single canonical endpoint", gotChat)
96 }
97 if strings.Count(gotModels, "/v1") != 1 || strings.Count(gotModels, "/models") != 1 {
98 t.Fatalf("models %q is not a single canonical endpoint", gotModels)
99 }
100 })
101 }
102 }
103
104 func TestNewCanonicalizesTokenRhythmChatURLs(t *testing.T) {
105 t.Parallel()
106
107 tests := []struct {
108 name string
109 cfg provider.Config
110 want string
111 }{
112 {
113 name: "request_url wins over legacy chat_url and base_url",
114 cfg: provider.Config{
115 Name: "律动",
116 BaseURL: "https://tokenrhythm.studio/v1/v1",
117 Model: "test-model",
118 Extra: map[string]any{
119 "chat_url": "https://legacy.example.com/chat/completions/",
120 "request_url": "https://exact.example.com/custom/?token=1",
121 },
122 },
123 want: "https://exact.example.com/custom/?token=1",
124 },
125 {
126 name: "legacy chat_url wins over base_url",
127 cfg: provider.Config{
128 Name: "other",
129 BaseURL: "https://base.example.com/v1",
130 Model: "test-model",
131 Extra: map[string]any{
132 "chat_url": "https://legacy.example.com/chat/completions/",
133 },
134 },
135 want: "https://legacy.example.com/chat/completions",
136 },
137 {
138 name: "third-party base_url still appends chat completions",
139 cfg: provider.Config{
140 Name: "other",
141 BaseURL: "https://base.example.com/v1",
142 Model: "test-model",
143 },
144 want: "https://base.example.com/v1/chat/completions",
145 },
146 {
147 name: "request_url tokenrhythm typo is repaired",
148 cfg: provider.Config{
149 Name: "custom-gateway",
150 BaseURL: "https://example.invalid/v1",
151 Model: "test-model",
152 Extra: map[string]any{
153 "chat_url": "https://legacy.example.com/chat/completions",
154 "request_url": "https://tokenrhythm.studio/v1/v1/chat/completions",
155 },
156 },
157 want: wantTokenRhythmChat,
158 },
159 {
160 name: "legacy chat_url tokenrhythm typo is repaired",
161 cfg: provider.Config{
162 Name: "律动",
163 BaseURL: "https://example.invalid/v1",
164 Model: "test-model",
165 Extra: map[string]any{
166 "chat_url": "https://tokenrhythm.studio/chat/completions/",
167 },
168 },
169 want: wantTokenRhythmChat,
170 },
171 {
172 name: "base_url tokenrhythm typo is repaired",
173 cfg: provider.Config{
174 Name: "anything",
175 BaseURL: "https://tokenrhythm.studio/v1/chat/completions",
176 Model: "test-model",
177 },
178 want: wantTokenRhythmChat,
179 },
180 {
181 name: "missing v1 base_url is repaired",
182 cfg: provider.Config{
183 Name: "律动",
184 BaseURL: "https://tokenrhythm.studio",
185 Model: "test-model",
186 },
187 want: wantTokenRhythmChat,
188 },
189 {
190 name: "third-party request_url keeps query and trailing slash",
191 cfg: provider.Config{
192 Name: "other",
193 BaseURL: "https://tokenrhythm.studio/v1",
194 Model: "test-model",
195 Extra: map[string]any{
196 "request_url": "https://exact.example.com/custom/?token=1",
197 },
198 },
199 want: "https://exact.example.com/custom/?token=1",
200 },
201 {
202 name: "third-party request_url keeps trailing slash",
203 cfg: provider.Config{
204 Name: "other",
205 BaseURL: "https://base.example.com/v1",
206 Model: "test-model",
207 Extra: map[string]any{
208 "request_url": "https://exact.example.com/custom/",
209 },
210 },
211 want: "https://exact.example.com/custom/",
212 },
213 {
214 name: "unknown official path is not guessed",
215 cfg: provider.Config{
216 Name: "律动",
217 BaseURL: "https://tokenrhythm.studio/openai",
218 Model: "test-model",
219 },
220 want: "https://tokenrhythm.studio/openai/chat/completions",
221 },
222 {
223 name: "third-party query on official host is left alone",
224 cfg: provider.Config{
225 Name: "律动",
226 BaseURL: "https://example.invalid/v1",
227 Model: "test-model",
228 Extra: map[string]any{
229 "request_url": "https://tokenrhythm.studio/v1/chat/completions?trace=1",
230 },
231 },
232 want: "https://tokenrhythm.studio/v1/chat/completions?trace=1",
233 },
234 }
235
236 for _, tt := range tests {
237 t.Run(tt.name, func(t *testing.T) {
238 t.Parallel()
239 p, err := New(tt.cfg)
240 if err != nil {
241 t.Fatalf("New: %v", err)
242 }
243 got := p.(*client).chatURL
244 if got != tt.want {
245 t.Fatalf("chatURL = %q, want %q", got, tt.want)
246 }
247 })
248 }
249 }
250
250 lines GO