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,270 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerStatsRoutes = registerStatsRoutes;
4
+ const statsRepository_1 = require("../storage/statsRepository");
5
+ async function registerStatsRoutes(app, paths) {
6
+ // GET /admin/stats - aggregated statistics
7
+ app.get("/admin/stats", async (req, reply) => {
8
+ const windowMs = parseWindow(req.query.window ?? "24h");
9
+ const timeZone = normalizeTimeZone(req.query.timeZone);
10
+ if (windowMs === null) {
11
+ reply.code(400).send({
12
+ error: {
13
+ message: "Invalid window format. Use format like '1h', '24h', '7d'"
14
+ }
15
+ });
16
+ return;
17
+ }
18
+ try {
19
+ const stats = await (0, statsRepository_1.aggregateStats)(paths, windowMs);
20
+ reply.send({
21
+ ...stats,
22
+ timeZone,
23
+ });
24
+ }
25
+ catch (error) {
26
+ app.log.error({ error }, "Failed to aggregate stats");
27
+ reply.code(500).send({ error: { message: "Failed to retrieve statistics" } });
28
+ }
29
+ });
30
+ // GET /admin/stats/raw - raw stats entries
31
+ app.get("/admin/stats/raw", async (req, reply) => {
32
+ const windowDays = parseWindowDays(req.query.window ?? "1d");
33
+ const limit = Math.min(parseInt(req.query.limit ?? "1000", 10), 10000);
34
+ if (windowDays === null) {
35
+ reply.code(400).send({
36
+ error: { message: "Invalid window format" }
37
+ });
38
+ return;
39
+ }
40
+ try {
41
+ const stats = await (0, statsRepository_1.readStatsForWindow)(paths, windowDays);
42
+ // Return most recent entries up to limit
43
+ const entries = stats.slice(-limit);
44
+ reply.send({
45
+ window: `${windowDays}d`,
46
+ count: entries.length,
47
+ totalInWindow: stats.length,
48
+ entries
49
+ });
50
+ }
51
+ catch (error) {
52
+ app.log.error({ error }, "Failed to read raw stats");
53
+ reply.code(500).send({ error: { message: "Failed to retrieve statistics" } });
54
+ }
55
+ });
56
+ // GET /admin/stats/latency - latency distribution
57
+ app.get("/admin/stats/latency", async (req, reply) => {
58
+ const windowMs = parseWindow(req.query.window ?? "7d");
59
+ const timeZone = normalizeTimeZone(req.query.timeZone);
60
+ if (windowMs === null) {
61
+ reply.code(400).send({ error: { message: "Invalid window format" } });
62
+ return;
63
+ }
64
+ try {
65
+ const stats = await selectStatsForWindow(paths, windowMs);
66
+ const latencies = stats.map((s) => s.latencyMs).sort((a, b) => a - b);
67
+ const window = formatWindowString(windowMs);
68
+ if (latencies.length === 0) {
69
+ reply.send({
70
+ window,
71
+ timeZone,
72
+ count: 0,
73
+ min: null,
74
+ max: null,
75
+ avg: null,
76
+ p50: null,
77
+ p95: null,
78
+ p99: null,
79
+ histogram: {}
80
+ });
81
+ return;
82
+ }
83
+ // Create histogram buckets
84
+ const buckets = [50, 100, 200, 500, 1000, 2000, 5000, 10000];
85
+ const histogram = {};
86
+ for (const bucket of buckets) {
87
+ histogram[`<${bucket}ms`] = 0;
88
+ }
89
+ histogram[">10000ms"] = 0;
90
+ for (const latency of latencies) {
91
+ let assigned = false;
92
+ for (const bucket of buckets) {
93
+ if (latency < bucket) {
94
+ histogram[`<${bucket}ms`]++;
95
+ assigned = true;
96
+ break;
97
+ }
98
+ }
99
+ if (!assigned) {
100
+ histogram[">10000ms"]++;
101
+ }
102
+ }
103
+ reply.send({
104
+ window,
105
+ timeZone,
106
+ count: latencies.length,
107
+ min: latencies[0],
108
+ max: latencies[latencies.length - 1],
109
+ avg: Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length),
110
+ p50: percentile(latencies, 50),
111
+ p95: percentile(latencies, 95),
112
+ p99: percentile(latencies, 99),
113
+ histogram
114
+ });
115
+ }
116
+ catch (error) {
117
+ app.log.error({ error }, "Failed to compute latency distribution");
118
+ reply.code(500).send({ error: { message: "Failed to retrieve statistics" } });
119
+ }
120
+ });
121
+ // GET /admin/stats/tokens - token usage over time
122
+ app.get("/admin/stats/tokens", async (req, reply) => {
123
+ const windowMs = parseWindow(req.query.window ?? "7d");
124
+ const timeZone = normalizeTimeZone(req.query.timeZone);
125
+ if (windowMs === null) {
126
+ reply.code(400).send({ error: { message: "Invalid window format" } });
127
+ return;
128
+ }
129
+ try {
130
+ const stats = await selectStatsForWindow(paths, windowMs);
131
+ const bucketGranularity = windowMs <= 24 * 60 * 60 * 1000 ? "hour" : "day";
132
+ const byDay = {};
133
+ let tokenEstimatedCount = 0;
134
+ let splitUnknownCount = 0;
135
+ let totalInputTokens = 0;
136
+ let totalOutputTokens = 0;
137
+ for (const stat of stats) {
138
+ const bucket = formatTokenBucket(stat.timestamp, bucketGranularity, timeZone);
139
+ if (!byDay[bucket]) {
140
+ byDay[bucket] = {
141
+ count: 0,
142
+ tokens: 0,
143
+ estimated: 0,
144
+ inputTokens: 0,
145
+ outputTokens: 0,
146
+ splitUnknown: 0,
147
+ };
148
+ }
149
+ byDay[bucket].count++;
150
+ if (stat.totalTokens !== null && stat.totalTokens !== undefined) {
151
+ byDay[bucket].tokens += stat.totalTokens;
152
+ }
153
+ else {
154
+ byDay[bucket].estimated++;
155
+ tokenEstimatedCount += 1;
156
+ }
157
+ const promptTokens = stat.promptTokens;
158
+ const completionTokens = stat.completionTokens;
159
+ const hasSplit = promptTokens !== null &&
160
+ promptTokens !== undefined &&
161
+ completionTokens !== null &&
162
+ completionTokens !== undefined;
163
+ if (hasSplit) {
164
+ byDay[bucket].inputTokens += promptTokens;
165
+ byDay[bucket].outputTokens += completionTokens;
166
+ totalInputTokens += promptTokens;
167
+ totalOutputTokens += completionTokens;
168
+ }
169
+ else {
170
+ byDay[bucket].splitUnknown++;
171
+ splitUnknownCount += 1;
172
+ }
173
+ }
174
+ const days = Object.entries(byDay)
175
+ .sort(([a], [b]) => a.localeCompare(b))
176
+ .map(([date, data]) => ({ date, ...data }));
177
+ const totalTokens = stats.reduce((sum, s) => sum + (s.totalTokens ?? 0), 0);
178
+ reply.send({
179
+ window: formatWindowString(windowMs),
180
+ totalTokens,
181
+ totalInputTokens,
182
+ totalOutputTokens,
183
+ totalRequests: stats.length,
184
+ avgTokensPerRequest: stats.length > 0 ? Math.round(totalTokens / stats.length) : 0,
185
+ byDay: days,
186
+ tokenEstimatedCount,
187
+ tokenEstimatedRate: stats.length > 0 ? tokenEstimatedCount / stats.length : 0,
188
+ splitUnknownCount,
189
+ splitUnknownRate: stats.length > 0 ? splitUnknownCount / stats.length : 0,
190
+ bucketGranularity,
191
+ bucketTimeZone: timeZone,
192
+ });
193
+ }
194
+ catch (error) {
195
+ app.log.error({ error }, "Failed to compute token usage");
196
+ reply.code(500).send({ error: { message: "Failed to retrieve statistics" } });
197
+ }
198
+ });
199
+ }
200
+ function parseWindow(window) {
201
+ const match = window.match(/^(\d+)(h|d|m)$/);
202
+ if (!match)
203
+ return null;
204
+ const value = parseInt(match[1], 10);
205
+ const unit = match[2];
206
+ switch (unit) {
207
+ case "m": return value * 60 * 1000;
208
+ case "h": return value * 60 * 60 * 1000;
209
+ case "d": return value * 24 * 60 * 60 * 1000;
210
+ default: return null;
211
+ }
212
+ }
213
+ function parseWindowDays(window) {
214
+ const ms = parseWindow(window);
215
+ if (ms === null)
216
+ return null;
217
+ return Math.ceil(ms / (24 * 60 * 60 * 1000));
218
+ }
219
+ function percentile(sortedArr, p) {
220
+ if (sortedArr.length === 0)
221
+ return 0;
222
+ const index = Math.ceil((p / 100) * sortedArr.length) - 1;
223
+ return sortedArr[Math.max(0, index)];
224
+ }
225
+ async function selectStatsForWindow(paths, windowMs) {
226
+ const windowDays = Math.ceil(windowMs / (24 * 60 * 60 * 1000));
227
+ const stats = await (0, statsRepository_1.readStatsForWindow)(paths, windowDays);
228
+ const cutoff = Date.now() - windowMs;
229
+ return stats.filter((s) => s.timestamp.getTime() >= cutoff);
230
+ }
231
+ function formatWindowString(ms) {
232
+ const hours = ms / (60 * 60 * 1000);
233
+ if (hours < 24)
234
+ return `${Math.round(hours)}h`;
235
+ return `${Math.round(hours / 24)}d`;
236
+ }
237
+ function formatTokenBucket(timestamp, granularity, timeZone) {
238
+ const formatter = new Intl.DateTimeFormat("en-CA", {
239
+ timeZone,
240
+ year: "numeric",
241
+ month: "2-digit",
242
+ day: "2-digit",
243
+ ...(granularity === "hour"
244
+ ? {
245
+ hour: "2-digit",
246
+ hourCycle: "h23",
247
+ }
248
+ : {}),
249
+ });
250
+ const parts = formatter.formatToParts(timestamp);
251
+ const year = parts.find((part) => part.type === "year")?.value ?? "0000";
252
+ const month = parts.find((part) => part.type === "month")?.value ?? "00";
253
+ const day = parts.find((part) => part.type === "day")?.value ?? "00";
254
+ if (granularity === "day") {
255
+ return `${year}-${month}-${day}`;
256
+ }
257
+ const hour = parts.find((part) => part.type === "hour")?.value ?? "00";
258
+ return `${year}-${month}-${day}T${hour}:00`;
259
+ }
260
+ function normalizeTimeZone(input) {
261
+ if (!input)
262
+ return "UTC";
263
+ try {
264
+ new Intl.DateTimeFormat("en-US", { timeZone: input });
265
+ return input;
266
+ }
267
+ catch {
268
+ return "UTC";
269
+ }
270
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerUiRoutes = registerUiRoutes;
7
+ const static_1 = __importDefault(require("@fastify/static"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = require("fs");
10
+ /**
11
+ * Register UI routes to serve the React frontend.
12
+ *
13
+ * - Serves static files from ui/dist at /ui/*
14
+ * - Provides SPA fallback for client-side routing
15
+ */
16
+ async function registerUiRoutes(app) {
17
+ // When running from dist/src/routes/ui.js, we need to go up to project root
18
+ // __dirname = dist/src/routes -> go up 3 levels to project root, then into ui/dist
19
+ const uiDistPath = path_1.default.join(__dirname, "..", "..", "..", "ui", "dist");
20
+ // Check if UI is built
21
+ const uiExists = await checkUiExists(uiDistPath);
22
+ if (!uiExists) {
23
+ // UI not built - serve a placeholder
24
+ app.get("/ui", async (_req, reply) => {
25
+ reply.type("text/html").send(`
26
+ <!DOCTYPE html>
27
+ <html>
28
+ <head>
29
+ <title>Waypoi UI</title>
30
+ <style>
31
+ body {
32
+ font-family: monospace;
33
+ background: #0a0a0c;
34
+ color: #e5e2d9;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ height: 100vh;
39
+ margin: 0;
40
+ }
41
+ .container { text-align: center; }
42
+ h1 { color: #eab308; }
43
+ code { background: #1a1a1e; padding: 4px 8px; border-radius: 4px; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <div class="container">
48
+ <h1>Waypoi UI</h1>
49
+ <p>UI not built. Run:</p>
50
+ <p><code>cd ui && npm install && npm run build</code></p>
51
+ <p>Then restart the server.</p>
52
+ </div>
53
+ </body>
54
+ </html>
55
+ `);
56
+ });
57
+ app.get("/ui/*", async (_req, reply) => {
58
+ reply.redirect("/ui");
59
+ });
60
+ return;
61
+ }
62
+ // Register static file serving
63
+ await app.register(static_1.default, {
64
+ root: uiDistPath,
65
+ prefix: "/ui/",
66
+ decorateReply: false,
67
+ });
68
+ // SPA fallback - serve index.html for all /ui/* routes that don't match a file
69
+ app.setNotFoundHandler(async (req, reply) => {
70
+ if (req.url.startsWith("/ui")) {
71
+ const indexPath = path_1.default.join(uiDistPath, "index.html");
72
+ try {
73
+ const html = await fs_1.promises.readFile(indexPath, "utf8");
74
+ reply.type("text/html").send(html);
75
+ }
76
+ catch {
77
+ reply.code(404).send({ error: { message: "UI not found" } });
78
+ }
79
+ }
80
+ else if (req.url === "/" && req.headers.accept?.includes("text/html")) {
81
+ reply.redirect("/ui");
82
+ }
83
+ else {
84
+ reply.code(404).send({ error: { message: "Not found" } });
85
+ }
86
+ });
87
+ }
88
+ async function checkUiExists(distPath) {
89
+ try {
90
+ const indexPath = path_1.default.join(distPath, "index.html");
91
+ await fs_1.promises.access(indexPath);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerVideoRoutes = registerVideoRoutes;
4
+ const crypto_1 = require("crypto");
5
+ const repositories_1 = require("../storage/repositories");
6
+ const videoGeneration_1 = require("../services/videoGeneration");
7
+ const requestCapture_1 = require("../middleware/requestCapture");
8
+ async function registerVideoRoutes(app, paths) {
9
+ app.post("/v1/videos/generations", async (req, reply) => {
10
+ const body = req.body;
11
+ if (!body?.prompt) {
12
+ reply.code(400).send({ error: { message: "prompt is required" } });
13
+ return;
14
+ }
15
+ const model = await (0, videoGeneration_1.resolveVideoGenerationModel)(paths, body.model);
16
+ if (!model) {
17
+ reply.code(400).send({ error: { message: "No video generation model available. Add or enable a provider model." } });
18
+ return;
19
+ }
20
+ const requestId = (0, crypto_1.randomUUID)();
21
+ const start = Date.now();
22
+ const controller = new AbortController();
23
+ req.raw.on("close", () => controller.abort());
24
+ try {
25
+ const generated = await (0, videoGeneration_1.runVideoGeneration)(paths, { ...body, model }, req.headers, controller.signal);
26
+ setHeaders(reply, generated.headers);
27
+ reply.code(generated.statusCode).send(generated.payload);
28
+ (0, requestCapture_1.setCaptureRouting)(reply, {
29
+ publicModel: model,
30
+ endpointId: generated.route.endpointId,
31
+ endpointName: generated.route.endpointName,
32
+ upstreamModel: generated.route.upstreamModel,
33
+ });
34
+ await (0, repositories_1.logRequest)(paths, buildLog(requestId, model, {
35
+ attempt: {
36
+ endpoint: {
37
+ id: generated.route.endpointId,
38
+ name: generated.route.endpointName,
39
+ },
40
+ upstreamModel: generated.route.upstreamModel,
41
+ response: {
42
+ statusCode: generated.statusCode,
43
+ },
44
+ },
45
+ }, Date.now() - start, false));
46
+ }
47
+ catch (error) {
48
+ const errorType = error.type ?? error.name;
49
+ (0, requestCapture_1.setCaptureError)(reply, { type: errorType, message: error.message });
50
+ await (0, repositories_1.logRequest)(paths, {
51
+ requestId,
52
+ ts: new Date(),
53
+ route: { publicModel: model },
54
+ request: { stream: false },
55
+ result: {
56
+ errorType,
57
+ errorMessage: error.message
58
+ }
59
+ });
60
+ if (errorType === "invalid_request") {
61
+ reply.code(400).send({ error: { message: error.message } });
62
+ return;
63
+ }
64
+ if (errorType === "tls_verify_failed") {
65
+ reply.code(502).send({ error: { message: error.message, type: errorType } });
66
+ return;
67
+ }
68
+ const status = errorType === "no_endpoints" ||
69
+ errorType === "protocol_stream_unsupported" ||
70
+ errorType === "unsupported_protocol" ||
71
+ errorType === "invalid_protocol_config"
72
+ ? 400
73
+ : errorType === "rate_limited"
74
+ ? 429
75
+ : 502;
76
+ reply.code(status).send({ error: { message: "Video generation unavailable", type: errorType } });
77
+ }
78
+ });
79
+ }
80
+ function setHeaders(reply, headers) {
81
+ for (const [key, value] of Object.entries(headers)) {
82
+ if (Array.isArray(value)) {
83
+ reply.header(key.toLowerCase(), value.join(", "));
84
+ }
85
+ else {
86
+ reply.header(key.toLowerCase(), value);
87
+ }
88
+ }
89
+ }
90
+ function buildLog(requestId, model, outcome, latencyMs, stream) {
91
+ return {
92
+ requestId,
93
+ ts: new Date(),
94
+ route: {
95
+ publicModel: model,
96
+ endpointId: outcome.attempt.endpoint.id,
97
+ endpointName: outcome.attempt.endpoint.name,
98
+ upstreamModel: outcome.attempt.upstreamModel
99
+ },
100
+ request: { stream },
101
+ result: {
102
+ statusCode: outcome.attempt.response.statusCode,
103
+ latencyMs,
104
+ totalTokens: null
105
+ }
106
+ };
107
+ }