| 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 |