waypoi 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,501 @@
1
+ import { EndpointDoc, EndpointType, ModelModality, UpstreamError, UpstreamResult } from "../types";
2
+ import { classifyHttpStatus, classifyUpstreamError, proxyUpstream } from "../transport/httpClient";
3
+ import { StoragePaths } from "../storage/files";
4
+ import {
5
+ buildEndpointFromCandidate,
6
+ estimateTokensFromPayload,
7
+ markPoolAttempt,
8
+ markPoolFailure,
9
+ markPoolSuccess,
10
+ selectPoolCandidates,
11
+ } from "../pools/scheduler";
12
+ import { PoolCandidate } from "../pools/types";
13
+ import { getProtocolAdapter, routePathToOperation } from "../protocols/registry";
14
+ import { PreparedUpstreamRequest, ProtocolAdapter, ProtocolNormalizeResponseContext } from "../protocols/types";
15
+ import { resolveModel } from "../providers/modelRegistry";
16
+ import { setProviderModelInsecureTls } from "../providers/repository";
17
+
18
+ export interface RouteAttempt {
19
+ endpoint: EndpointDoc;
20
+ upstreamModel: string;
21
+ response: UpstreamResult;
22
+ pool?: {
23
+ id: string;
24
+ alias: string;
25
+ candidateAttempts: number;
26
+ failovers: number;
27
+ rateLimitSwitches: number;
28
+ distinctProviders: number;
29
+ distinctModels: number;
30
+ };
31
+ }
32
+
33
+ export interface RouteOutcome {
34
+ attempt: RouteAttempt;
35
+ retryable: boolean;
36
+ errorType?: string;
37
+ }
38
+
39
+ interface RouteRequirements {
40
+ endpointType?: EndpointType;
41
+ requiredInput?: ModelModality[];
42
+ requiredOutput?: ModelModality[];
43
+ }
44
+
45
+ export async function routeRequest(
46
+ paths: StoragePaths,
47
+ publicModel: string,
48
+ path: string,
49
+ payload: Record<string, unknown>,
50
+ headers: Record<string, string | string[] | undefined>,
51
+ signal: AbortSignal,
52
+ requirements?: RouteRequirements
53
+ ): Promise<RouteOutcome> {
54
+ const operation = routePathToOperation(path);
55
+ const streamRequested = payload.stream === true;
56
+ const resolved = await resolveModel(
57
+ paths,
58
+ publicModel,
59
+ {
60
+ requiredInput: requirements?.requiredInput,
61
+ requiredOutput: requirements?.requiredOutput,
62
+ },
63
+ operation ? { operation, stream: streamRequested } : undefined
64
+ );
65
+
66
+ if (resolved.kind === "ambiguous") {
67
+ const error = new Error(
68
+ `Model '${resolved.input}' is ambiguous. Use canonical model ID: ${resolved.matches.join(", ")}`
69
+ ) as UpstreamError;
70
+ error.type = "invalid_request";
71
+ error.retryable = false;
72
+ throw error;
73
+ }
74
+ if (resolved.kind === "deprecated_pool_alias") {
75
+ const error = new Error(
76
+ `Model alias '${resolved.input}' is deprecated. Use '${resolved.replacement}' instead.`
77
+ ) as UpstreamError;
78
+ error.type = "invalid_request";
79
+ error.retryable = false;
80
+ throw error;
81
+ }
82
+ if (resolved.kind === "none") {
83
+ const error = new Error(`Unknown model '${resolved.input}'`) as UpstreamError;
84
+ error.type = "no_endpoints";
85
+ error.retryable = false;
86
+ throw error;
87
+ }
88
+
89
+ if (resolved.kind === "pool") {
90
+ const poolSelection = await selectPoolCandidates(
91
+ paths,
92
+ resolved.alias,
93
+ {
94
+ requiredInput: requirements?.requiredInput,
95
+ requiredOutput: requirements?.requiredOutput,
96
+ },
97
+ operation ? { operation, stream: streamRequested } : undefined
98
+ );
99
+ if (!poolSelection || poolSelection.candidates.length === 0) {
100
+ const exhaustedByLimits = poolSelection?.skipped.some(
101
+ (item) => item.reason === "cooldown" || item.reason === "request_budget_exhausted"
102
+ );
103
+ const streamUnsupported = poolSelection?.skipped.some(
104
+ (item) => item.reason === "stream_unsupported"
105
+ );
106
+ const error = new Error("No eligible endpoints for model") as UpstreamError;
107
+ error.type = exhaustedByLimits
108
+ ? "rate_limited"
109
+ : streamUnsupported
110
+ ? "protocol_stream_unsupported"
111
+ : "no_endpoints";
112
+ error.retryable = Boolean(exhaustedByLimits);
113
+ throw error;
114
+ }
115
+ return routeWithPoolCandidates(
116
+ paths,
117
+ resolved.alias,
118
+ path,
119
+ payload,
120
+ headers,
121
+ signal,
122
+ poolSelection.pool.id,
123
+ poolSelection.candidates,
124
+ requirements,
125
+ operation,
126
+ streamRequested
127
+ );
128
+ }
129
+
130
+ if (resolved.candidates.length === 0) {
131
+ const error = new Error("No eligible endpoints for model") as UpstreamError;
132
+ error.type = "no_endpoints";
133
+ error.retryable = false;
134
+ throw error;
135
+ }
136
+ return routeWithPoolCandidates(
137
+ paths,
138
+ resolved.canonicalId,
139
+ path,
140
+ payload,
141
+ headers,
142
+ signal,
143
+ `model:${resolved.canonicalId}`,
144
+ resolved.candidates,
145
+ requirements,
146
+ operation,
147
+ streamRequested
148
+ );
149
+ }
150
+
151
+ async function routeWithPoolCandidates(
152
+ paths: StoragePaths,
153
+ publicModel: string,
154
+ path: string,
155
+ payload: Record<string, unknown>,
156
+ headers: Record<string, string | string[] | undefined>,
157
+ signal: AbortSignal,
158
+ poolId: string,
159
+ candidates: PoolCandidate[],
160
+ requirements: RouteRequirements | undefined,
161
+ operation: ReturnType<typeof routePathToOperation>,
162
+ streamRequested: boolean
163
+ ): Promise<RouteOutcome> {
164
+ if (candidates.length === 0) {
165
+ const error = new Error("No eligible endpoints for model") as UpstreamError;
166
+ error.type = "no_endpoints";
167
+ error.retryable = false;
168
+ throw error;
169
+ }
170
+
171
+ let lastError: UpstreamError | null = null;
172
+ let attempts = 0;
173
+ let rateLimitSwitches = 0;
174
+ const seenProviders = new Set<string>();
175
+ const seenModels = new Set<string>();
176
+ const triedModels: string[] = [];
177
+ const triedModelSet = new Set<string>();
178
+ const isSmartAlias = publicModel === "smart";
179
+
180
+ for (const candidate of candidates) {
181
+ attempts += 1;
182
+ seenProviders.add(candidate.providerId);
183
+ seenModels.add(candidate.modelId);
184
+ const candidateName = `${candidate.providerId}/${candidate.modelId}`;
185
+ if (!triedModelSet.has(candidateName)) {
186
+ triedModelSet.add(candidateName);
187
+ triedModels.push(candidateName);
188
+ }
189
+ const endpoint = buildEndpointFromCandidate(candidate);
190
+ const adapter = getProtocolAdapter(candidate.protocol);
191
+ if (!adapter) {
192
+ continue;
193
+ }
194
+ const support = adapter.supports({
195
+ operation: operation ?? "chat_completions",
196
+ stream: streamRequested,
197
+ capabilities: candidate.capabilities,
198
+ requiredInput: requirements?.requiredInput,
199
+ requiredOutput: requirements?.requiredOutput,
200
+ });
201
+ if (!support.supported) {
202
+ continue;
203
+ }
204
+
205
+ const timeoutMs = candidate.limits?.timeoutMs ?? 60_000;
206
+ const start = Date.now();
207
+ await markPoolAttempt(paths, candidate, estimateTokensFromPayload(payload));
208
+ let requestData: PreparedUpstreamRequest | null = null;
209
+
210
+ try {
211
+ requestData = await adapter.buildRequest({
212
+ paths,
213
+ operation: operation ?? "chat_completions",
214
+ stream: streamRequested,
215
+ path,
216
+ payload: { ...payload, model: candidate.upstreamModel },
217
+ publicModel,
218
+ upstreamModel: candidate.upstreamModel,
219
+ endpoint,
220
+ auth: candidate.auth,
221
+ config: candidate.protocolConfig,
222
+ });
223
+
224
+ const response = await proxyUpstream(
225
+ endpoint,
226
+ requestData.path,
227
+ requestData.payload,
228
+ mergeForwardHeaders(headers, requestData.headers),
229
+ timeoutMs,
230
+ signal,
231
+ { skipDefaultAuth: requestData.skipDefaultAuth }
232
+ );
233
+ const latency = Date.now() - start;
234
+ const classification = classifyHttpStatus(response.statusCode);
235
+ if (isSmartAlias && shouldFailoverForIncompatibleStatus(response.statusCode)) {
236
+ await markPoolFailure(paths, candidate, {
237
+ error: `operation_unsupported_${response.statusCode}`,
238
+ headers: response.headers,
239
+ });
240
+ await drainBody(response.body);
241
+ lastError = new Error(`Incompatible status ${response.statusCode}`) as UpstreamError;
242
+ lastError.type = classification.type;
243
+ lastError.retryable = true;
244
+ continue;
245
+ }
246
+ if (classification.retryable) {
247
+ if (response.statusCode === 429) {
248
+ rateLimitSwitches += 1;
249
+ }
250
+ await markPoolFailure(paths, candidate, {
251
+ error: classification.type,
252
+ rateLimited: response.statusCode === 429,
253
+ headers: response.headers,
254
+ });
255
+ await drainBody(response.body);
256
+ lastError = new Error(`Retryable status ${response.statusCode}`) as UpstreamError;
257
+ lastError.type = classification.type;
258
+ lastError.retryable = true;
259
+ continue;
260
+ }
261
+
262
+ const normalizedResponse = await maybeNormalizeResponse(
263
+ adapter,
264
+ {
265
+ operation: operation ?? "chat_completions",
266
+ stream: streamRequested,
267
+ path,
268
+ publicModel,
269
+ upstreamModel: candidate.upstreamModel,
270
+ requestPayload: requestData.payload,
271
+ upstreamResult: response,
272
+ config: candidate.protocolConfig,
273
+ },
274
+ response
275
+ );
276
+
277
+ await markPoolSuccess(paths, candidate, latency);
278
+ return {
279
+ attempt: {
280
+ endpoint,
281
+ upstreamModel: candidate.upstreamModel,
282
+ response: normalizedResponse,
283
+ pool: {
284
+ id: poolId,
285
+ alias: publicModel,
286
+ candidateAttempts: attempts,
287
+ failovers: Math.max(0, attempts - 1),
288
+ rateLimitSwitches,
289
+ distinctProviders: seenProviders.size,
290
+ distinctModels: seenModels.size,
291
+ },
292
+ },
293
+ retryable: false,
294
+ };
295
+ } catch (error) {
296
+ let classified = classifyUpstreamError(error);
297
+
298
+ if (
299
+ classified.type === "tls_verify_failed" &&
300
+ endpoint.insecureTls !== true &&
301
+ requestData &&
302
+ isHostnameAllowlisted(candidate)
303
+ ) {
304
+ const insecureEndpoint = { ...endpoint, insecureTls: true };
305
+ await markPoolAttempt(paths, candidate, estimateTokensFromPayload(payload));
306
+ try {
307
+ const retryResponse = await proxyUpstream(
308
+ insecureEndpoint,
309
+ requestData.path,
310
+ requestData.payload,
311
+ mergeForwardHeaders(headers, requestData.headers),
312
+ timeoutMs,
313
+ signal,
314
+ { skipDefaultAuth: requestData.skipDefaultAuth }
315
+ );
316
+ const latency = Date.now() - start;
317
+ const retryClassification = classifyHttpStatus(retryResponse.statusCode);
318
+ if (isSmartAlias && shouldFailoverForIncompatibleStatus(retryResponse.statusCode)) {
319
+ await markPoolFailure(paths, candidate, {
320
+ error: `operation_unsupported_${retryResponse.statusCode}`,
321
+ headers: retryResponse.headers,
322
+ });
323
+ await drainBody(retryResponse.body);
324
+ lastError = new Error(`Incompatible status ${retryResponse.statusCode}`) as UpstreamError;
325
+ lastError.type = retryClassification.type;
326
+ lastError.retryable = true;
327
+ continue;
328
+ }
329
+ if (retryClassification.retryable) {
330
+ if (retryResponse.statusCode === 429) {
331
+ rateLimitSwitches += 1;
332
+ }
333
+ await markPoolFailure(paths, candidate, {
334
+ error: retryClassification.type,
335
+ rateLimited: retryResponse.statusCode === 429,
336
+ headers: retryResponse.headers,
337
+ });
338
+ await drainBody(retryResponse.body);
339
+ lastError = new Error(`Retryable status ${retryResponse.statusCode}`) as UpstreamError;
340
+ lastError.type = retryClassification.type;
341
+ lastError.retryable = true;
342
+ continue;
343
+ }
344
+
345
+ const normalizedRetryResponse = await maybeNormalizeResponse(
346
+ adapter,
347
+ {
348
+ operation: operation ?? "chat_completions",
349
+ stream: streamRequested,
350
+ path,
351
+ publicModel,
352
+ upstreamModel: candidate.upstreamModel,
353
+ requestPayload: requestData.payload,
354
+ upstreamResult: retryResponse,
355
+ config: candidate.protocolConfig,
356
+ },
357
+ retryResponse
358
+ );
359
+
360
+ if (retryClassification.type === "ok") {
361
+ await setProviderModelInsecureTls(
362
+ paths,
363
+ candidate.providerId,
364
+ candidate.providerModelId ?? candidate.modelId,
365
+ true
366
+ );
367
+ }
368
+
369
+ await markPoolSuccess(paths, candidate, latency);
370
+ return {
371
+ attempt: {
372
+ endpoint: insecureEndpoint,
373
+ upstreamModel: candidate.upstreamModel,
374
+ response: normalizedRetryResponse,
375
+ pool: {
376
+ id: poolId,
377
+ alias: publicModel,
378
+ candidateAttempts: attempts,
379
+ failovers: Math.max(0, attempts - 1),
380
+ rateLimitSwitches,
381
+ distinctProviders: seenProviders.size,
382
+ distinctModels: seenModels.size,
383
+ },
384
+ },
385
+ retryable: false,
386
+ };
387
+ } catch (retryError) {
388
+ classified = classifyUpstreamError(retryError);
389
+ }
390
+ }
391
+
392
+ if (classified.type === "tls_verify_failed") {
393
+ classified.message = tlsVerifyFailureMessage(candidate);
394
+ }
395
+ lastError = classified;
396
+ await markPoolFailure(paths, candidate, {
397
+ error: classified.type,
398
+ rateLimited: classified.type === "rate_limited",
399
+ });
400
+ if (!classified.retryable) {
401
+ throw maybeEnrichSmartError(classified, triedModels, poolId, isSmartAlias);
402
+ }
403
+ }
404
+ }
405
+
406
+ if (lastError) {
407
+ throw maybeEnrichSmartError(lastError, triedModels, poolId, isSmartAlias);
408
+ }
409
+ const error = new Error("No endpoints succeeded") as UpstreamError;
410
+ error.type = "no_endpoints";
411
+ error.retryable = true;
412
+ throw maybeEnrichSmartError(error, triedModels, poolId, isSmartAlias);
413
+ }
414
+
415
+ function shouldFailoverForIncompatibleStatus(statusCode: number): boolean {
416
+ return statusCode === 404 || statusCode === 405 || statusCode === 501;
417
+ }
418
+
419
+ function maybeEnrichSmartError(
420
+ error: UpstreamError,
421
+ triedModels: string[],
422
+ poolId: string,
423
+ shouldEnrich: boolean
424
+ ): UpstreamError {
425
+ if (!shouldEnrich) {
426
+ return error;
427
+ }
428
+ error.poolId = poolId;
429
+ error.triedModels = triedModels.slice(0, 10);
430
+ if (error.triedModels.length > 0) {
431
+ error.message = `Smart routing failed. Tried models: ${error.triedModels.join(", ")}. Cause: ${error.message}`;
432
+ } else {
433
+ error.message = `Smart routing failed. No eligible models. Cause: ${error.message}`;
434
+ }
435
+ return error;
436
+ }
437
+
438
+ function isHostnameAllowlisted(candidate: PoolCandidate): boolean {
439
+ const allowlist = candidate.autoInsecureTlsDomains ?? [];
440
+ if (allowlist.length === 0) {
441
+ return false;
442
+ }
443
+ const hostname = getHostname(candidate.baseUrl);
444
+ if (!hostname) {
445
+ return false;
446
+ }
447
+ return allowlist.some((suffix) => matchesDomainSuffix(hostname, suffix));
448
+ }
449
+
450
+ function getHostname(baseUrl: string): string | null {
451
+ try {
452
+ return new URL(baseUrl).hostname.toLowerCase();
453
+ } catch {
454
+ return null;
455
+ }
456
+ }
457
+
458
+ function matchesDomainSuffix(hostname: string, suffix: string): boolean {
459
+ const normalizedSuffix = suffix.trim().toLowerCase();
460
+ if (!normalizedSuffix) {
461
+ return false;
462
+ }
463
+ return hostname === normalizedSuffix || hostname.endsWith(`.${normalizedSuffix}`);
464
+ }
465
+
466
+ function tlsVerifyFailureMessage(candidate: PoolCandidate): string {
467
+ return `Upstream TLS verify failed for ${candidate.providerId}/${candidate.modelId}. Configure provider/model insecureTls or provider allowlist.`;
468
+ }
469
+
470
+ async function maybeNormalizeResponse(
471
+ adapter: ProtocolAdapter,
472
+ context: ProtocolNormalizeResponseContext,
473
+ fallback: UpstreamResult
474
+ ): Promise<UpstreamResult> {
475
+ if (!adapter.normalizeResponse) {
476
+ return fallback;
477
+ }
478
+ return adapter.normalizeResponse(context);
479
+ }
480
+
481
+ async function drainBody(stream: NodeJS.ReadableStream): Promise<void> {
482
+ return new Promise((resolve) => {
483
+ stream.on("end", resolve);
484
+ stream.on("close", resolve);
485
+ stream.on("error", resolve);
486
+ stream.resume();
487
+ });
488
+ }
489
+
490
+ function mergeForwardHeaders(
491
+ base: Record<string, string | string[] | undefined>,
492
+ extras?: Record<string, string>
493
+ ): Record<string, string | string[] | undefined> {
494
+ if (!extras || Object.keys(extras).length === 0) {
495
+ return base;
496
+ }
497
+ return {
498
+ ...base,
499
+ ...extras,
500
+ };
501
+ }