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,295 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAudioRoutes = registerAudioRoutes;
4
+ const crypto_1 = require("crypto");
5
+ const stream_1 = require("stream");
6
+ const router_1 = require("../routing/router");
7
+ const repositories_1 = require("../storage/repositories");
8
+ const scheduler_1 = require("../pools/scheduler");
9
+ const modelRegistry_1 = require("../providers/modelRegistry");
10
+ const requestCapture_1 = require("../middleware/requestCapture");
11
+ async function registerAudioRoutes(app, paths) {
12
+ // POST /v1/audio/transcriptions (speech-to-text)
13
+ app.post("/v1/audio/transcriptions", async (req, reply) => {
14
+ // Multipart form data: file, model, language, prompt, response_format, temperature
15
+ const body = req.body;
16
+ const model = body?.model ?? await pickDefaultAudioModel(paths);
17
+ if (!model) {
18
+ reply.code(400).send({ error: { message: "No audio model available. Add or enable a provider model." } });
19
+ return;
20
+ }
21
+ const requestId = (0, crypto_1.randomUUID)();
22
+ const start = Date.now();
23
+ const controller = new AbortController();
24
+ req.raw.on("close", () => controller.abort());
25
+ try {
26
+ const outcome = await (0, router_1.routeRequest)(paths, model, "/v1/audio/transcriptions", body, req.headers, controller.signal, {
27
+ endpointType: "audio",
28
+ requiredInput: ["audio"],
29
+ requiredOutput: ["text"],
30
+ });
31
+ const upstreamBody = await readBody(outcome.attempt.response);
32
+ setHeaders(reply, outcome.attempt.response.headers);
33
+ reply.code(outcome.attempt.response.statusCode).send(upstreamBody.payload);
34
+ (0, requestCapture_1.setCaptureRouting)(reply, {
35
+ publicModel: model,
36
+ endpointId: outcome.attempt.endpoint.id,
37
+ endpointName: outcome.attempt.endpoint.name,
38
+ upstreamModel: outcome.attempt.upstreamModel,
39
+ });
40
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, model, outcome, Date.now() - start));
41
+ }
42
+ catch (error) {
43
+ const errorType = error.type ?? error.name;
44
+ (0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
45
+ await (0, repositories_1.logRequest)(paths, {
46
+ requestId,
47
+ ts: new Date(),
48
+ route: { publicModel: model },
49
+ request: { stream: false },
50
+ result: { errorType, errorMessage: error.message }
51
+ });
52
+ if (errorType === "invalid_request") {
53
+ reply.code(400).send({ error: { message: error.message } });
54
+ return;
55
+ }
56
+ if (errorType === "tls_verify_failed") {
57
+ reply.code(502).send({ error: { message: error.message, type: errorType } });
58
+ return;
59
+ }
60
+ const status = errorType === "no_endpoints" ||
61
+ errorType === "protocol_stream_unsupported" ||
62
+ errorType === "unsupported_protocol" ||
63
+ errorType === "invalid_protocol_config"
64
+ ? 400
65
+ : errorType === "rate_limited"
66
+ ? 429
67
+ : 502;
68
+ reply.code(status).send({ error: { message: "Transcription unavailable", type: errorType } });
69
+ }
70
+ });
71
+ // POST /v1/audio/translations (translation to English)
72
+ app.post("/v1/audio/translations", async (req, reply) => {
73
+ const body = req.body;
74
+ const model = body?.model ?? await pickDefaultAudioModel(paths);
75
+ if (!model) {
76
+ reply.code(400).send({ error: { message: "No audio model available. Add or enable a provider model." } });
77
+ return;
78
+ }
79
+ const requestId = (0, crypto_1.randomUUID)();
80
+ const start = Date.now();
81
+ const controller = new AbortController();
82
+ req.raw.on("close", () => controller.abort());
83
+ try {
84
+ const outcome = await (0, router_1.routeRequest)(paths, model, "/v1/audio/translations", body, req.headers, controller.signal, {
85
+ endpointType: "audio",
86
+ requiredInput: ["audio"],
87
+ requiredOutput: ["text"],
88
+ });
89
+ const upstreamBody = await readBody(outcome.attempt.response);
90
+ setHeaders(reply, outcome.attempt.response.headers);
91
+ reply.code(outcome.attempt.response.statusCode).send(upstreamBody.payload);
92
+ (0, requestCapture_1.setCaptureRouting)(reply, {
93
+ publicModel: model,
94
+ endpointId: outcome.attempt.endpoint.id,
95
+ endpointName: outcome.attempt.endpoint.name,
96
+ upstreamModel: outcome.attempt.upstreamModel,
97
+ });
98
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, model, outcome, Date.now() - start));
99
+ }
100
+ catch (error) {
101
+ const errorType = error.type ?? error.name;
102
+ (0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
103
+ await (0, repositories_1.logRequest)(paths, {
104
+ requestId,
105
+ ts: new Date(),
106
+ route: { publicModel: model },
107
+ request: { stream: false },
108
+ result: { errorType, errorMessage: error.message }
109
+ });
110
+ if (errorType === "invalid_request") {
111
+ reply.code(400).send({ error: { message: error.message } });
112
+ return;
113
+ }
114
+ if (errorType === "tls_verify_failed") {
115
+ reply.code(502).send({ error: { message: error.message, type: errorType } });
116
+ return;
117
+ }
118
+ const status = errorType === "no_endpoints" ||
119
+ errorType === "protocol_stream_unsupported" ||
120
+ errorType === "unsupported_protocol" ||
121
+ errorType === "invalid_protocol_config"
122
+ ? 400
123
+ : errorType === "rate_limited"
124
+ ? 429
125
+ : 502;
126
+ reply.code(status).send({ error: { message: "Translation unavailable", type: errorType } });
127
+ }
128
+ });
129
+ // POST /v1/audio/speech (text-to-speech)
130
+ app.post("/v1/audio/speech", async (req, reply) => {
131
+ const body = req.body;
132
+ if (!body?.input || !body?.voice) {
133
+ reply.code(400).send({ error: { message: "input and voice are required" } });
134
+ return;
135
+ }
136
+ const model = body.model ?? await pickDefaultTtsModel(paths);
137
+ if (!model) {
138
+ reply.code(400).send({ error: { message: "No TTS model available. Add or enable a text-to-audio provider model." } });
139
+ return;
140
+ }
141
+ const requestId = (0, crypto_1.randomUUID)();
142
+ const start = Date.now();
143
+ const controller = new AbortController();
144
+ req.raw.on("close", () => controller.abort());
145
+ try {
146
+ const outcome = await (0, router_1.routeRequest)(paths, model, "/v1/audio/speech", { ...body, model }, req.headers, controller.signal, {
147
+ endpointType: "audio",
148
+ requiredInput: ["text"],
149
+ requiredOutput: ["audio"],
150
+ });
151
+ // Speech returns binary audio - stream it directly
152
+ await streamResponse(reply, outcome.attempt.response);
153
+ (0, requestCapture_1.setCaptureResponseOverride)(reply, {
154
+ $type: "stream",
155
+ contentType: normalizeHeaders(outcome.attempt.response.headers)["content-type"] ?? "application/octet-stream",
156
+ note: "Audio stream captured as metadata",
157
+ }, outcome.attempt.response.headers);
158
+ (0, requestCapture_1.setCaptureRouting)(reply, {
159
+ publicModel: model,
160
+ endpointId: outcome.attempt.endpoint.id,
161
+ endpointName: outcome.attempt.endpoint.name,
162
+ upstreamModel: outcome.attempt.upstreamModel,
163
+ });
164
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, model, outcome, Date.now() - start));
165
+ }
166
+ catch (error) {
167
+ const errorType = error.type ?? error.name;
168
+ (0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
169
+ await (0, repositories_1.logRequest)(paths, {
170
+ requestId,
171
+ ts: new Date(),
172
+ route: { publicModel: model },
173
+ request: { stream: false },
174
+ result: { errorType, errorMessage: error.message }
175
+ });
176
+ // Don't try to send error if headers already sent (streaming started)
177
+ if (reply.raw.headersSent) {
178
+ req.log.warn({ err: error }, "Error after streaming started");
179
+ reply.raw.end();
180
+ return;
181
+ }
182
+ if (errorType === "invalid_request") {
183
+ reply.code(400).send({ error: { message: error.message } });
184
+ return;
185
+ }
186
+ if (errorType === "tls_verify_failed") {
187
+ reply.code(502).send({ error: { message: error.message, type: errorType } });
188
+ return;
189
+ }
190
+ const status = errorType === "no_endpoints" ||
191
+ errorType === "protocol_stream_unsupported" ||
192
+ errorType === "unsupported_protocol" ||
193
+ errorType === "invalid_protocol_config"
194
+ ? 400
195
+ : errorType === "rate_limited"
196
+ ? 429
197
+ : 502;
198
+ reply.code(status).send({ error: { message: "Speech synthesis unavailable", type: errorType } });
199
+ }
200
+ });
201
+ }
202
+ async function pickDefaultAudioModel(paths) {
203
+ const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
204
+ requiredInput: ["audio"],
205
+ requiredOutput: ["text"],
206
+ }, {
207
+ operation: "audio_transcriptions",
208
+ stream: false,
209
+ });
210
+ if (smart && smart.candidates.length > 0) {
211
+ return "smart";
212
+ }
213
+ const byCapabilities = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["audio"], requiredOutput: ["text"] }, "audio");
214
+ if (byCapabilities) {
215
+ return byCapabilities;
216
+ }
217
+ return null;
218
+ }
219
+ async function pickDefaultTtsModel(paths) {
220
+ const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
221
+ requiredInput: ["text"],
222
+ requiredOutput: ["audio"],
223
+ }, {
224
+ operation: "audio_speech",
225
+ stream: false,
226
+ });
227
+ if (smart && smart.candidates.length > 0) {
228
+ return "smart";
229
+ }
230
+ const byCapabilities = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["text"], requiredOutput: ["audio"] }, "audio");
231
+ if (byCapabilities) {
232
+ return byCapabilities;
233
+ }
234
+ return null;
235
+ }
236
+ async function streamResponse(reply, response) {
237
+ const headers = normalizeHeaders(response.headers);
238
+ reply.raw.writeHead(response.statusCode, headers);
239
+ await new Promise((resolve, reject) => {
240
+ (0, stream_1.pipeline)(response.body, reply.raw, (err) => {
241
+ if (err)
242
+ reject(err);
243
+ else
244
+ resolve();
245
+ });
246
+ });
247
+ }
248
+ function setHeaders(reply, headers) {
249
+ const normalized = normalizeHeaders(headers);
250
+ for (const [key, value] of Object.entries(normalized)) {
251
+ reply.header(key, value);
252
+ }
253
+ }
254
+ function normalizeHeaders(headers) {
255
+ const normalized = {};
256
+ for (const [key, value] of Object.entries(headers)) {
257
+ normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
258
+ }
259
+ return normalized;
260
+ }
261
+ async function readBody(response) {
262
+ const chunks = [];
263
+ for await (const chunk of response.body) {
264
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
265
+ }
266
+ const buffer = Buffer.concat(chunks);
267
+ const contentType = normalizeHeaders(response.headers)["content-type"] ?? "";
268
+ if (contentType.includes("application/json")) {
269
+ try {
270
+ return { payload: JSON.parse(buffer.toString("utf8")) };
271
+ }
272
+ catch {
273
+ return { payload: buffer };
274
+ }
275
+ }
276
+ return { payload: buffer };
277
+ }
278
+ function buildLog(requestId, model, outcome, latencyMs) {
279
+ return {
280
+ requestId,
281
+ ts: new Date(),
282
+ route: {
283
+ publicModel: model,
284
+ endpointId: outcome.attempt.endpoint.id,
285
+ endpointName: outcome.attempt.endpoint.name,
286
+ upstreamModel: outcome.attempt.upstreamModel
287
+ },
288
+ request: { stream: false },
289
+ result: {
290
+ statusCode: outcome.attempt.response.statusCode,
291
+ latencyMs,
292
+ totalTokens: null
293
+ }
294
+ };
295
+ }
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerChatRoutes = registerChatRoutes;
4
+ const crypto_1 = require("crypto");
5
+ const stream_1 = require("stream");
6
+ const router_1 = require("../routing/router");
7
+ const repositories_1 = require("../storage/repositories");
8
+ const scheduler_1 = require("../pools/scheduler");
9
+ const modelRegistry_1 = require("../providers/modelRegistry");
10
+ const messageMedia_1 = require("../utils/messageMedia");
11
+ const requestCapture_1 = require("../middleware/requestCapture");
12
+ const requestStats_1 = require("../middleware/requestStats");
13
+ const stream_2 = require("stream");
14
+ async function registerChatRoutes(app, paths) {
15
+ app.post("/v1/chat/completions", async (req, reply) => {
16
+ let body = req.body;
17
+ if (!body?.model) {
18
+ const fallback = await pickDefaultModel(paths);
19
+ if (!fallback) {
20
+ reply.code(400).send({ error: { message: "model is required" } });
21
+ return;
22
+ }
23
+ body = { ...(body ?? {}), model: fallback };
24
+ }
25
+ const requestId = (0, crypto_1.randomUUID)();
26
+ const start = Date.now();
27
+ const controller = new AbortController();
28
+ req.raw.on("close", () => controller.abort());
29
+ try {
30
+ const messages = body.messages;
31
+ const normalizedMessages = await (0, messageMedia_1.normalizeMessagesForUpstream)(paths, messages);
32
+ const bodyWithNormalizedMessages = {
33
+ ...body,
34
+ messages: normalizedMessages,
35
+ };
36
+ (0, requestCapture_1.setCaptureDerivedRequest)(reply, { normalizedRequest: bodyWithNormalizedMessages });
37
+ const media = (0, messageMedia_1.scanMessageModalities)(normalizedMessages);
38
+ const outcome = await (0, router_1.routeRequest)(paths, body.model, "/v1/chat/completions", bodyWithNormalizedMessages, req.headers, controller.signal, {
39
+ requiredInput: media.hasAudio
40
+ ? media.hasImage
41
+ ? ["text", "image", "audio"]
42
+ : ["text", "audio"]
43
+ : media.hasImage
44
+ ? ["text", "image"]
45
+ : ["text"],
46
+ requiredOutput: ["text"],
47
+ });
48
+ if (body.stream) {
49
+ (0, requestCapture_1.setCaptureRouting)(reply, {
50
+ publicModel: body.model,
51
+ endpointId: outcome.attempt.endpoint.id,
52
+ endpointName: outcome.attempt.endpoint.name,
53
+ upstreamModel: outcome.attempt.upstreamModel,
54
+ });
55
+ (0, requestStats_1.setStatsPayload)(reply, {
56
+ endpointId: outcome.attempt.endpoint.id,
57
+ endpointName: outcome.attempt.endpoint.name,
58
+ upstreamModel: outcome.attempt.upstreamModel,
59
+ });
60
+ await streamResponse(reply, outcome.attempt.response);
61
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, body, outcome, Date.now() - start));
62
+ return;
63
+ }
64
+ const upstreamBody = await readBody(outcome.attempt.response);
65
+ (0, requestCapture_1.setCaptureResponseOverride)(reply, upstreamBody.payload, outcome.attempt.response.headers);
66
+ setHeaders(reply, outcome.attempt.response.headers);
67
+ reply.code(outcome.attempt.response.statusCode).send(upstreamBody.payload);
68
+ (0, requestCapture_1.setCaptureRouting)(reply, {
69
+ publicModel: body.model,
70
+ endpointId: outcome.attempt.endpoint.id,
71
+ endpointName: outcome.attempt.endpoint.name,
72
+ upstreamModel: outcome.attempt.upstreamModel,
73
+ });
74
+ (0, requestStats_1.setStatsPayload)(reply, {
75
+ endpointId: outcome.attempt.endpoint.id,
76
+ endpointName: outcome.attempt.endpoint.name,
77
+ upstreamModel: outcome.attempt.upstreamModel,
78
+ totalTokens: upstreamBody.totalTokens,
79
+ promptTokens: upstreamBody.promptTokens,
80
+ completionTokens: upstreamBody.completionTokens,
81
+ });
82
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, body, outcome, Date.now() - start, upstreamBody.totalTokens));
83
+ }
84
+ catch (error) {
85
+ const errorType = error.type ?? error.name;
86
+ (0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
87
+ await (0, repositories_1.logRequest)(paths, {
88
+ requestId,
89
+ ts: new Date(),
90
+ route: { publicModel: body?.model ?? "unknown" },
91
+ request: { stream: Boolean(body?.stream), maxTokens: body?.max_tokens },
92
+ result: {
93
+ errorType,
94
+ errorMessage: error.message
95
+ }
96
+ });
97
+ // Don't try to send error if headers already sent (streaming started)
98
+ if (reply.raw.headersSent) {
99
+ req.log.warn({ err: error }, "Error after streaming started");
100
+ reply.raw.end();
101
+ return;
102
+ }
103
+ const status = errorType === "no_endpoints" ||
104
+ errorType === "protocol_stream_unsupported" ||
105
+ errorType === "unsupported_protocol" ||
106
+ errorType === "invalid_protocol_config"
107
+ ? 400
108
+ : errorType === "rate_limited"
109
+ ? 429
110
+ : 502;
111
+ if (errorType === "invalid_request") {
112
+ reply.code(400).send({ error: { message: error.message } });
113
+ return;
114
+ }
115
+ if (errorType === "tls_verify_failed") {
116
+ reply.code(502).send({ error: { message: error.message } });
117
+ return;
118
+ }
119
+ reply.code(status).send({ error: { message: "Upstream unavailable" } });
120
+ }
121
+ });
122
+ }
123
+ async function pickDefaultModel(paths) {
124
+ const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
125
+ requiredInput: ["text"],
126
+ requiredOutput: ["text"],
127
+ }, {
128
+ operation: "chat_completions",
129
+ stream: false,
130
+ });
131
+ if (smart && smart.candidates.length > 0) {
132
+ return "smart";
133
+ }
134
+ const byCapabilities = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["text"], requiredOutput: ["text"] }, "llm");
135
+ if (byCapabilities) {
136
+ return byCapabilities;
137
+ }
138
+ return null;
139
+ }
140
+ async function streamResponse(reply, response) {
141
+ const headers = normalizeHeaders(response.headers);
142
+ if (!headers["content-type"]) {
143
+ headers["content-type"] = "text/event-stream";
144
+ }
145
+ headers["cache-control"] = headers["cache-control"] ?? "no-cache";
146
+ const contentType = headers["content-type"] ?? "application/octet-stream";
147
+ (0, requestCapture_1.startCaptureStreamResponse)(reply, headers, contentType);
148
+ const chunks = [];
149
+ const captureTap = new stream_2.Transform({
150
+ transform(chunk, _enc, cb) {
151
+ const asBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
152
+ chunks.push(asBuffer);
153
+ (0, requestCapture_1.appendCaptureStreamChunk)(reply, asBuffer, { contentType, headers });
154
+ cb(null, chunk);
155
+ },
156
+ });
157
+ reply.raw.writeHead(response.statusCode, headers);
158
+ await new Promise((resolve, reject) => {
159
+ (0, stream_1.pipeline)(response.body, captureTap, reply.raw, (err) => {
160
+ if (err) {
161
+ reject(err);
162
+ return;
163
+ }
164
+ resolve();
165
+ });
166
+ });
167
+ const buffer = Buffer.concat(chunks);
168
+ const isText = contentType.includes("text/") || contentType.includes("json") || contentType.includes("event-stream");
169
+ (0, requestCapture_1.setCaptureResponseOverride)(reply, {
170
+ $type: "stream",
171
+ contentType,
172
+ bytes: buffer.byteLength,
173
+ text: isText ? buffer.toString("utf8") : undefined,
174
+ }, headers);
175
+ return {
176
+ bytes: buffer.byteLength,
177
+ text: isText ? buffer.toString("utf8") : undefined,
178
+ contentType,
179
+ };
180
+ }
181
+ function setHeaders(reply, headers) {
182
+ const normalized = normalizeHeaders(headers);
183
+ for (const [key, value] of Object.entries(normalized)) {
184
+ reply.header(key, value);
185
+ }
186
+ }
187
+ function normalizeHeaders(headers) {
188
+ const normalized = {};
189
+ for (const [key, value] of Object.entries(headers)) {
190
+ if (Array.isArray(value)) {
191
+ normalized[key.toLowerCase()] = value.join(", ");
192
+ }
193
+ else {
194
+ normalized[key.toLowerCase()] = value;
195
+ }
196
+ }
197
+ return normalized;
198
+ }
199
+ async function readBody(response) {
200
+ const chunks = [];
201
+ for await (const chunk of response.body) {
202
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
203
+ }
204
+ const buffer = Buffer.concat(chunks);
205
+ const contentType = normalizeHeaders(response.headers)["content-type"] ?? "";
206
+ if (contentType.includes("application/json")) {
207
+ try {
208
+ const payload = JSON.parse(buffer.toString("utf8"));
209
+ const usage = typeof payload === "object" && payload && payload.usage;
210
+ return {
211
+ payload,
212
+ totalTokens: usage?.total_tokens ?? null,
213
+ promptTokens: usage?.prompt_tokens ?? null,
214
+ completionTokens: usage?.completion_tokens ?? null,
215
+ };
216
+ }
217
+ catch {
218
+ return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
219
+ }
220
+ }
221
+ return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
222
+ }
223
+ function buildLog(requestId, body, outcome, latencyMs, totalTokens) {
224
+ return {
225
+ requestId,
226
+ ts: new Date(),
227
+ route: {
228
+ publicModel: body.model,
229
+ endpointId: outcome.attempt.endpoint.id,
230
+ endpointName: outcome.attempt.endpoint.name,
231
+ upstreamModel: outcome.attempt.upstreamModel
232
+ },
233
+ request: { stream: Boolean(body.stream), maxTokens: body.max_tokens },
234
+ result: {
235
+ statusCode: outcome.attempt.response.statusCode,
236
+ latencyMs,
237
+ totalTokens: totalTokens ?? null
238
+ }
239
+ };
240
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerEmbeddingsRoutes = registerEmbeddingsRoutes;
4
+ const crypto_1 = require("crypto");
5
+ const router_1 = require("../routing/router");
6
+ const repositories_1 = require("../storage/repositories");
7
+ const scheduler_1 = require("../pools/scheduler");
8
+ const modelRegistry_1 = require("../providers/modelRegistry");
9
+ const requestCapture_1 = require("../middleware/requestCapture");
10
+ const requestStats_1 = require("../middleware/requestStats");
11
+ async function registerEmbeddingsRoutes(app, paths) {
12
+ app.post("/v1/embeddings", async (req, reply) => {
13
+ let body = req.body;
14
+ if (!body?.model) {
15
+ const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
16
+ requiredInput: ["text"],
17
+ requiredOutput: ["embedding"],
18
+ }, {
19
+ operation: "embeddings",
20
+ stream: false,
21
+ });
22
+ if (smart && smart.candidates.length > 0) {
23
+ body = { ...(body ?? { input: "" }), model: "smart" };
24
+ }
25
+ }
26
+ if (!body?.model) {
27
+ const direct = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["text"], requiredOutput: ["embedding"] }, "embedding");
28
+ if (direct) {
29
+ body = { ...(body ?? { input: "" }), model: direct };
30
+ }
31
+ }
32
+ if (!body?.model) {
33
+ reply.code(400).send({ error: { message: "model is required" } });
34
+ return;
35
+ }
36
+ const requestId = (0, crypto_1.randomUUID)();
37
+ const start = Date.now();
38
+ const controller = new AbortController();
39
+ req.raw.on("close", () => controller.abort());
40
+ try {
41
+ const outcome = await (0, router_1.routeRequest)(paths, body.model, "/v1/embeddings", body, req.headers, controller.signal, {
42
+ endpointType: "embedding",
43
+ requiredInput: ["text"],
44
+ requiredOutput: ["embedding"],
45
+ });
46
+ const upstreamBody = await readBody(outcome.attempt.response);
47
+ setHeaders(reply, outcome.attempt.response.headers);
48
+ reply.code(outcome.attempt.response.statusCode).send(upstreamBody.payload);
49
+ (0, requestCapture_1.setCaptureRouting)(reply, {
50
+ publicModel: body.model,
51
+ endpointId: outcome.attempt.endpoint.id,
52
+ endpointName: outcome.attempt.endpoint.name,
53
+ upstreamModel: outcome.attempt.upstreamModel,
54
+ });
55
+ (0, requestStats_1.setStatsPayload)(reply, {
56
+ endpointId: outcome.attempt.endpoint.id,
57
+ endpointName: outcome.attempt.endpoint.name,
58
+ upstreamModel: outcome.attempt.upstreamModel,
59
+ totalTokens: upstreamBody.totalTokens,
60
+ promptTokens: upstreamBody.promptTokens,
61
+ completionTokens: upstreamBody.completionTokens,
62
+ });
63
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, body, outcome, Date.now() - start, upstreamBody.totalTokens));
64
+ }
65
+ catch (error) {
66
+ const errorType = error.type ?? error.name;
67
+ (0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
68
+ await (0, repositories_1.logRequest)(paths, {
69
+ requestId,
70
+ ts: new Date(),
71
+ route: { publicModel: body?.model ?? "unknown" },
72
+ request: { stream: false },
73
+ result: {
74
+ errorType,
75
+ errorMessage: error.message
76
+ }
77
+ });
78
+ if (errorType === "invalid_request") {
79
+ reply.code(400).send({ error: { message: error.message } });
80
+ return;
81
+ }
82
+ if (errorType === "tls_verify_failed") {
83
+ reply.code(502).send({ error: { message: error.message } });
84
+ return;
85
+ }
86
+ const status = errorType === "no_endpoints" ||
87
+ errorType === "protocol_stream_unsupported" ||
88
+ errorType === "unsupported_protocol" ||
89
+ errorType === "invalid_protocol_config"
90
+ ? 400
91
+ : errorType === "rate_limited"
92
+ ? 429
93
+ : 502;
94
+ reply.code(status).send({ error: { message: "Upstream unavailable" } });
95
+ }
96
+ });
97
+ }
98
+ function setHeaders(reply, headers) {
99
+ const normalized = normalizeHeaders(headers);
100
+ for (const [key, value] of Object.entries(normalized)) {
101
+ reply.header(key, value);
102
+ }
103
+ }
104
+ function normalizeHeaders(headers) {
105
+ const normalized = {};
106
+ for (const [key, value] of Object.entries(headers)) {
107
+ if (Array.isArray(value)) {
108
+ normalized[key.toLowerCase()] = value.join(", ");
109
+ }
110
+ else {
111
+ normalized[key.toLowerCase()] = value;
112
+ }
113
+ }
114
+ return normalized;
115
+ }
116
+ async function readBody(response) {
117
+ const chunks = [];
118
+ for await (const chunk of response.body) {
119
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
120
+ }
121
+ const buffer = Buffer.concat(chunks);
122
+ const contentType = normalizeHeaders(response.headers)["content-type"] ?? "";
123
+ if (contentType.includes("application/json")) {
124
+ try {
125
+ const payload = JSON.parse(buffer.toString("utf8"));
126
+ const usage = typeof payload === "object" && payload && payload.usage;
127
+ return {
128
+ payload,
129
+ totalTokens: usage?.total_tokens ?? null,
130
+ promptTokens: usage?.prompt_tokens ?? null,
131
+ completionTokens: usage?.completion_tokens ?? null,
132
+ };
133
+ }
134
+ catch {
135
+ return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
136
+ }
137
+ }
138
+ return { payload: buffer, totalTokens: null, promptTokens: null, completionTokens: null };
139
+ }
140
+ function buildLog(requestId, body, outcome, latencyMs, totalTokens) {
141
+ return {
142
+ requestId,
143
+ ts: new Date(),
144
+ route: {
145
+ publicModel: body.model,
146
+ endpointId: outcome.attempt.endpoint.id,
147
+ endpointName: outcome.attempt.endpoint.name,
148
+ upstreamModel: outcome.attempt.upstreamModel
149
+ },
150
+ request: { stream: false },
151
+ result: {
152
+ statusCode: outcome.attempt.response.statusCode,
153
+ latencyMs,
154
+ totalTokens: totalTokens ?? null
155
+ }
156
+ };
157
+ }