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,666 @@
1
+ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
2
+ import { StoragePaths } from "../storage/files";
3
+ import { listAllProtocolAdapters } from "../protocols/registry";
4
+ import {
5
+ deleteProvider,
6
+ deleteProviderModel,
7
+ getEffectiveModelInsecureTls,
8
+ getProviderById,
9
+ listProviderModels,
10
+ listProviders,
11
+ setProviderEnabled,
12
+ setProviderModelEnabled,
13
+ updateProviderModel,
14
+ updateProvider,
15
+ upsertProvider,
16
+ upsertProviderModel,
17
+ } from "../providers/repository";
18
+ import { ProviderModelRecord, ProviderRecord } from "../providers/types";
19
+ import { listPools, savePools } from "../pools/repository";
20
+ import { PoolDefinition } from "../pools/types";
21
+ import { rebuildDefaultPools } from "../pools/builder";
22
+ import { BenchmarkCliOptions } from "../benchmark/types";
23
+ import { listBenchmarkExamples } from "../benchmark/runner";
24
+ import {
25
+ getArtifactBenchmarkRun,
26
+ getBenchmarkRun,
27
+ hasRunningBenchmarkRun,
28
+ listBenchmarkRunEvents,
29
+ listBenchmarkRuns,
30
+ startBenchmarkRun,
31
+ subscribeBenchmarkRunEvents,
32
+ } from "../benchmark/jobs";
33
+ import { getCapabilitySnapshotByModel, listCapabilitySnapshots, toCapabilityMatrix } from "../benchmark/capabilityStore";
34
+ import { discoverUpstreamModels } from "../utils/modelDiscovery";
35
+ import {
36
+ findCaptureBlobPath,
37
+ getCaptureCalendarMonth,
38
+ getCaptureConfig,
39
+ getCaptureRecordById,
40
+ listCaptureRecords,
41
+ updateCaptureConfig,
42
+ } from "../storage/captureRepository";
43
+ import { promises as fs } from "fs";
44
+
45
+ interface AdminEnv {
46
+ adminToken?: string;
47
+ version?: string;
48
+ }
49
+
50
+ interface ProviderModelPayload {
51
+ providerModelId?: string;
52
+ modelId?: string;
53
+ upstreamModel?: string;
54
+ baseUrl?: string;
55
+ apiKey?: string;
56
+ insecureTls?: boolean;
57
+ enabled?: boolean;
58
+ aliases?: string[];
59
+ free?: boolean;
60
+ modalities?: string[];
61
+ capabilities?: ProviderModelRecord["capabilities"];
62
+ endpointType?: ProviderModelRecord["endpointType"];
63
+ limits?: ProviderModelRecord["limits"];
64
+ }
65
+
66
+ interface ProviderModelDiscoveryPayload {
67
+ baseUrl?: string;
68
+ apiKey?: string;
69
+ insecureTls?: boolean;
70
+ }
71
+
72
+ interface ProviderPayload {
73
+ id?: string;
74
+ name?: string;
75
+ description?: string;
76
+ docs?: string;
77
+ protocol?: ProviderRecord["protocol"];
78
+ protocolRaw?: string;
79
+ protocolConfig?: ProviderRecord["protocolConfig"];
80
+ baseUrl?: string;
81
+ insecureTls?: boolean;
82
+ autoInsecureTlsDomains?: string[];
83
+ enabled?: boolean;
84
+ supportsRouting?: boolean;
85
+ auth?: ProviderRecord["auth"];
86
+ envVar?: string;
87
+ apiKey?: string;
88
+ limits?: ProviderRecord["limits"];
89
+ }
90
+
91
+ export async function registerAdminRoutes(app: FastifyInstance, paths: StoragePaths, env: AdminEnv): Promise<void> {
92
+ app.addHook("onRequest", async (req, reply) => {
93
+ if (!req.url.startsWith("/admin")) {
94
+ return;
95
+ }
96
+ if (!isAuthorized(req, env.adminToken)) {
97
+ reply.code(401).send({ error: { message: "Unauthorized" } });
98
+ return reply;
99
+ }
100
+ });
101
+
102
+ app.get("/admin/meta", async (_req, reply) => {
103
+ reply.send({
104
+ name: "waypoi",
105
+ version: env.version ?? "0.0.0",
106
+ now: new Date().toISOString(),
107
+ });
108
+ });
109
+
110
+ app.get("/admin/protocols", async (_req, reply) => {
111
+ reply.send({ data: listAllProtocolAdapters() });
112
+ });
113
+
114
+ app.get("/admin/providers", async (_req, reply) => {
115
+ const providers = await listProviders(paths);
116
+ reply.send(providers);
117
+ });
118
+
119
+ app.get("/admin/providers/:id", async (req, reply) => {
120
+ const { id } = req.params as { id: string };
121
+ const provider = await getProviderById(paths, id);
122
+ if (!provider) {
123
+ reply.code(404).send({ error: { message: "provider not found" } });
124
+ return;
125
+ }
126
+ reply.send(provider);
127
+ });
128
+
129
+ app.post("/admin/providers", async (req, reply) => {
130
+ const body = req.body as ProviderPayload | undefined;
131
+ if (!body?.id || !body?.baseUrl || !body?.protocol) {
132
+ reply.code(400).send({ error: { message: "id, baseUrl, and protocol are required" } });
133
+ return;
134
+ }
135
+ const provider: ProviderRecord = {
136
+ id: body.id,
137
+ name: body.name ?? body.id,
138
+ description: body.description,
139
+ docs: body.docs,
140
+ protocol: body.protocol,
141
+ protocolRaw: body.protocolRaw,
142
+ protocolConfig: body.protocolConfig,
143
+ baseUrl: body.baseUrl,
144
+ insecureTls: body.insecureTls,
145
+ autoInsecureTlsDomains: body.autoInsecureTlsDomains ?? [],
146
+ enabled: body.enabled ?? true,
147
+ supportsRouting: body.supportsRouting ?? true,
148
+ auth: body.auth,
149
+ envVar: body.envVar,
150
+ apiKey: body.apiKey,
151
+ limits: body.limits,
152
+ models: [],
153
+ importedAt: new Date().toISOString(),
154
+ };
155
+ const saved = await upsertProvider(paths, provider);
156
+ await rebuildDefaultPools(paths);
157
+ reply.code(201).send(saved);
158
+ });
159
+
160
+ app.patch("/admin/providers/:id", async (req, reply) => {
161
+ const { id } = req.params as { id: string };
162
+ const body = req.body as ProviderPayload | undefined;
163
+ if (!body) {
164
+ reply.code(400).send({ error: { message: "payload required" } });
165
+ return;
166
+ }
167
+ const updated = await updateProvider(paths, id, body as Partial<ProviderRecord>);
168
+ if (!updated) {
169
+ reply.code(404).send({ error: { message: "provider not found" } });
170
+ return;
171
+ }
172
+ await rebuildDefaultPools(paths);
173
+ reply.send(updated);
174
+ });
175
+
176
+ app.delete("/admin/providers/:id", async (req, reply) => {
177
+ const { id } = req.params as { id: string };
178
+ const removed = await deleteProvider(paths, id);
179
+ if (!removed) {
180
+ reply.code(404).send({ error: { message: "provider not found" } });
181
+ return;
182
+ }
183
+ await rebuildDefaultPools(paths);
184
+ reply.send({ deleted: removed.id });
185
+ });
186
+
187
+ app.post("/admin/providers/:id/enable", async (req, reply) => {
188
+ const { id } = req.params as { id: string };
189
+ const provider = await setProviderEnabled(paths, id, true);
190
+ if (!provider) {
191
+ reply.code(404).send({ error: { message: "provider not found" } });
192
+ return;
193
+ }
194
+ await rebuildDefaultPools(paths);
195
+ reply.send(provider);
196
+ });
197
+
198
+ app.post("/admin/providers/:id/disable", async (req, reply) => {
199
+ const { id } = req.params as { id: string };
200
+ const provider = await setProviderEnabled(paths, id, false);
201
+ if (!provider) {
202
+ reply.code(404).send({ error: { message: "provider not found" } });
203
+ return;
204
+ }
205
+ await rebuildDefaultPools(paths);
206
+ reply.send(provider);
207
+ });
208
+
209
+ app.get("/admin/providers/:id/models", async (req, reply) => {
210
+ const { id } = req.params as { id: string };
211
+ const models = await listProviderModels(paths, id);
212
+ if (!models) {
213
+ reply.code(404).send({ error: { message: "provider not found" } });
214
+ return;
215
+ }
216
+ reply.send(models);
217
+ });
218
+
219
+ app.post("/admin/providers/:id/models/discover", async (req, reply) => {
220
+ const { id } = req.params as { id: string };
221
+ const body = req.body as ProviderModelDiscoveryPayload | undefined;
222
+ const provider = await getProviderById(paths, id);
223
+ if (!provider) {
224
+ reply.code(404).send({ error: { message: "provider not found" } });
225
+ return;
226
+ }
227
+
228
+ const baseUrl = body?.baseUrl?.trim() || provider.baseUrl;
229
+ if (!baseUrl) {
230
+ reply.code(400).send({ error: { message: "baseUrl is required" } });
231
+ return;
232
+ }
233
+
234
+ try {
235
+ const models = await discoverUpstreamModels({
236
+ baseUrl,
237
+ apiKey: body?.apiKey?.trim() || provider.apiKey,
238
+ insecureTls:
239
+ body?.insecureTls === true
240
+ ? true
241
+ : getEffectiveModelInsecureTls(provider, { insecureTls: undefined }),
242
+ auth: provider.auth,
243
+ });
244
+ reply.send({ baseUrl, models });
245
+ } catch (error) {
246
+ const message = error instanceof Error && error.message ? error.message : "model discovery failed";
247
+ reply.code(502).send({ error: { message } });
248
+ }
249
+ });
250
+
251
+ app.post("/admin/providers/:id/models", async (req, reply) => {
252
+ const { id } = req.params as { id: string };
253
+ const body = req.body as ProviderModelPayload | undefined;
254
+ if (!body?.modelId || !body?.upstreamModel || !body?.endpointType || !body?.capabilities) {
255
+ reply.code(400).send({
256
+ error: {
257
+ message:
258
+ "modelId, upstreamModel, endpointType, and capabilities are required",
259
+ },
260
+ });
261
+ return;
262
+ }
263
+ const provider = await getProviderById(paths, id);
264
+ if (!provider) {
265
+ reply.code(404).send({ error: { message: "provider not found" } });
266
+ return;
267
+ }
268
+ const record: ProviderModelRecord = {
269
+ providerModelId: body.providerModelId ?? `${id}/${body.modelId}`,
270
+ providerId: id,
271
+ modelId: body.modelId,
272
+ upstreamModel: body.upstreamModel,
273
+ baseUrl: body.baseUrl || provider.baseUrl,
274
+ apiKey: body.apiKey,
275
+ insecureTls: body.insecureTls,
276
+ enabled: body.enabled ?? true,
277
+ aliases: body.aliases ?? [],
278
+ free: body.free ?? true,
279
+ modalities: body.modalities ?? [],
280
+ capabilities: body.capabilities,
281
+ endpointType: body.endpointType,
282
+ limits: body.limits,
283
+ };
284
+ const result = await upsertProviderModel(paths, id, record);
285
+ if (!result) {
286
+ reply.code(500).send({ error: { message: "failed to add model" } });
287
+ return;
288
+ }
289
+ await rebuildDefaultPools(paths);
290
+ reply.code(result.created ? 201 : 200).send(record);
291
+ });
292
+
293
+ app.patch("/admin/providers/:id/models/:modelRef", async (req, reply) => {
294
+ const { id, modelRef } = req.params as { id: string; modelRef: string };
295
+ const body = req.body as ProviderModelPayload | undefined;
296
+ if (!body) {
297
+ reply.code(400).send({ error: { message: "payload required" } });
298
+ return;
299
+ }
300
+ const updated = await updateProviderModel(paths, id, modelRef, body as Partial<ProviderModelRecord>);
301
+ if (!updated) {
302
+ reply.code(404).send({ error: { message: "model not found" } });
303
+ return;
304
+ }
305
+ await rebuildDefaultPools(paths);
306
+ reply.send(updated);
307
+ });
308
+
309
+ app.delete("/admin/providers/:id/models/:modelRef", async (req, reply) => {
310
+ const { id, modelRef } = req.params as { id: string; modelRef: string };
311
+ const removed = await deleteProviderModel(paths, id, modelRef);
312
+ if (!removed) {
313
+ reply.code(404).send({ error: { message: "model not found" } });
314
+ return;
315
+ }
316
+ await rebuildDefaultPools(paths);
317
+ reply.send({ deleted: removed.providerModelId });
318
+ });
319
+
320
+ app.post("/admin/providers/:id/models/:modelRef/enable", async (req, reply) => {
321
+ const { id, modelRef } = req.params as { id: string; modelRef: string };
322
+ const model = await setProviderModelEnabled(paths, id, modelRef, true);
323
+ if (!model) {
324
+ reply.code(404).send({ error: { message: "model not found" } });
325
+ return;
326
+ }
327
+ await rebuildDefaultPools(paths);
328
+ reply.send(model);
329
+ });
330
+
331
+ app.post("/admin/providers/:id/models/:modelRef/disable", async (req, reply) => {
332
+ const { id, modelRef } = req.params as { id: string; modelRef: string };
333
+ const model = await setProviderModelEnabled(paths, id, modelRef, false);
334
+ if (!model) {
335
+ reply.code(404).send({ error: { message: "model not found" } });
336
+ return;
337
+ }
338
+ await rebuildDefaultPools(paths);
339
+ reply.send(model);
340
+ });
341
+
342
+ app.get("/admin/pools", async (_req, reply) => {
343
+ const pools = await listPools(paths);
344
+ reply.send(pools);
345
+ });
346
+
347
+ app.post(
348
+ "/admin/pools",
349
+ async (req: FastifyRequest<{ Body: Partial<PoolDefinition> }>, reply: FastifyReply) => {
350
+ const body = req.body ?? {};
351
+ if (!body.id?.trim()) {
352
+ reply.code(400).send({ error: { message: "Pool ID is required" } });
353
+ return;
354
+ }
355
+ const existing = await listPools(paths);
356
+ if (existing.some((p) => p.id === body.id)) {
357
+ reply.code(409).send({ error: { message: "A pool with this ID already exists" } });
358
+ return;
359
+ }
360
+ const pool: PoolDefinition = {
361
+ id: body.id.trim(),
362
+ name: body.name?.trim() || body.id.trim(),
363
+ aliases: body.aliases ?? [body.id.trim()],
364
+ enabled: body.enabled !== false,
365
+ strategy: (body.strategy as PoolDefinition["strategy"]) ?? "highest_rank_available",
366
+ requiredInput: body.requiredInput ?? [],
367
+ requiredOutput: body.requiredOutput ?? [],
368
+ scoreFallback: typeof body.scoreFallback === "number" ? body.scoreFallback : 20,
369
+ candidates: [],
370
+ candidateSelection: body.candidateSelection ?? [],
371
+ userDefined: true,
372
+ updatedAt: new Date().toISOString(),
373
+ };
374
+ existing.push(pool);
375
+ await savePools(paths, existing);
376
+ reply.code(201).send(pool);
377
+ }
378
+ );
379
+
380
+ app.put(
381
+ "/admin/pools/:id",
382
+ async (req: FastifyRequest<{ Params: { id: string }; Body: Partial<PoolDefinition> }>, reply: FastifyReply) => {
383
+ const { id } = req.params;
384
+ const body = req.body ?? {};
385
+ const pools = await listPools(paths);
386
+ const idx = pools.findIndex((p) => p.id === id);
387
+ if (idx === -1) {
388
+ reply.code(404).send({ error: { message: "Pool not found" } });
389
+ return;
390
+ }
391
+ const existing = pools[idx];
392
+ if (!existing.userDefined) {
393
+ reply.code(403).send({ error: { message: "Cannot modify auto-generated pools" } });
394
+ return;
395
+ }
396
+ pools[idx] = {
397
+ ...existing,
398
+ name: body.name !== undefined ? body.name.trim() : existing.name,
399
+ aliases: body.aliases ?? existing.aliases,
400
+ enabled: body.enabled !== undefined ? body.enabled : existing.enabled,
401
+ strategy: (body.strategy as PoolDefinition["strategy"]) ?? existing.strategy,
402
+ requiredInput: body.requiredInput ?? existing.requiredInput,
403
+ requiredOutput: body.requiredOutput ?? existing.requiredOutput,
404
+ scoreFallback: typeof body.scoreFallback === "number" ? body.scoreFallback : existing.scoreFallback,
405
+ candidateSelection: body.candidateSelection ?? existing.candidateSelection,
406
+ updatedAt: new Date().toISOString(),
407
+ };
408
+ await savePools(paths, pools);
409
+ reply.send(pools[idx]);
410
+ }
411
+ );
412
+
413
+ app.delete("/admin/pools/:id", async (req: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
414
+ const { id } = req.params;
415
+ const pools = await listPools(paths);
416
+ const idx = pools.findIndex((p) => p.id === id);
417
+ if (idx === -1) {
418
+ reply.code(404).send({ error: { message: "Pool not found" } });
419
+ return;
420
+ }
421
+ if (!pools[idx].userDefined) {
422
+ reply.code(403).send({ error: { message: "Cannot delete auto-generated pools" } });
423
+ return;
424
+ }
425
+ pools.splice(idx, 1);
426
+ await savePools(paths, pools);
427
+ reply.send({ deleted: id });
428
+ });
429
+
430
+ app.post("/admin/pools/:id/toggle", async (req: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
431
+ const { id } = req.params;
432
+ const pools = await listPools(paths);
433
+ const pool = pools.find((p) => p.id === id);
434
+ if (!pool) {
435
+ reply.code(404).send({ error: { message: "Pool not found" } });
436
+ return;
437
+ }
438
+ pool.enabled = !pool.enabled;
439
+ pool.updatedAt = new Date().toISOString();
440
+ await savePools(paths, pools);
441
+ reply.send(pool);
442
+ });
443
+
444
+ app.post("/admin/pools/rebuild", async (_req, reply) => {
445
+ const pools = await rebuildDefaultPools(paths);
446
+ reply.send({ rebuilt: pools.length, pools });
447
+ });
448
+
449
+ app.post(
450
+ "/admin/benchmarks/runs",
451
+ async (req: FastifyRequest<{ Body: BenchmarkCliOptions }>, reply: FastifyReply) => {
452
+ if (hasRunningBenchmarkRun()) {
453
+ reply.code(409).send({
454
+ error: { message: "A benchmark run is already in progress" },
455
+ });
456
+ return;
457
+ }
458
+ const body = req.body ?? {};
459
+ const run = await startBenchmarkRun(paths, {
460
+ suite: body.suite,
461
+ exampleId: body.exampleId,
462
+ scenarioPath: body.scenarioPath,
463
+ modelOverride: body.modelOverride,
464
+ outPath: body.outPath,
465
+ configPath: body.configPath,
466
+ profile: body.profile,
467
+ baselinePath: body.baselinePath,
468
+ executionMode: body.executionMode,
469
+ updateCapCache: body.updateCapCache,
470
+ capTtlDays: body.capTtlDays,
471
+ temperature: body.temperature,
472
+ top_p: body.top_p,
473
+ max_tokens: body.max_tokens,
474
+ presence_penalty: body.presence_penalty,
475
+ frequency_penalty: body.frequency_penalty,
476
+ seed: body.seed,
477
+ stop: body.stop,
478
+ });
479
+ reply.code(202).send(run);
480
+ }
481
+ );
482
+
483
+ app.get("/admin/benchmarks/runs", async (_req, reply) => {
484
+ const items = await listBenchmarkRuns(paths);
485
+ reply.send({
486
+ object: "list",
487
+ data: items,
488
+ });
489
+ });
490
+
491
+ app.get("/admin/benchmarks/examples", async (req, reply) => {
492
+ const suite = ((req.query as { suite?: string } | undefined)?.suite ?? "showcase").trim() || "showcase";
493
+ reply.send({
494
+ object: "list",
495
+ suite,
496
+ data: listBenchmarkExamples(suite),
497
+ });
498
+ });
499
+
500
+ app.get("/admin/benchmarks/runs/:id", async (req, reply) => {
501
+ const { id } = req.params as { id: string };
502
+ const run = getBenchmarkRun(id);
503
+ if (run) {
504
+ reply.send(run);
505
+ return;
506
+ }
507
+ const historical = await getArtifactBenchmarkRun(paths, id);
508
+ if (!historical) {
509
+ reply.code(404).send({ error: { message: "benchmark run not found" } });
510
+ return;
511
+ }
512
+ reply.send(historical);
513
+ });
514
+
515
+ app.get("/admin/benchmarks/runs/:id/events", async (req, reply) => {
516
+ const { id } = req.params as { id: string };
517
+ const run = getBenchmarkRun(id);
518
+ if (!run) {
519
+ reply.code(404).send({ error: { message: "benchmark run not found" } });
520
+ return;
521
+ }
522
+
523
+ reply.raw.writeHead(200, {
524
+ "Content-Type": "text/event-stream",
525
+ "Cache-Control": "no-cache, no-transform",
526
+ Connection: "keep-alive",
527
+ "X-Accel-Buffering": "no",
528
+ });
529
+
530
+ const sendEvent = (event: unknown): void => {
531
+ reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
532
+ };
533
+
534
+ for (const event of listBenchmarkRunEvents(id)) {
535
+ sendEvent(event);
536
+ }
537
+
538
+ const unsubscribe = subscribeBenchmarkRunEvents(id, (event) => {
539
+ sendEvent(event);
540
+ });
541
+
542
+ const heartbeat = setInterval(() => {
543
+ reply.raw.write(": ping\n\n");
544
+ }, 15_000);
545
+
546
+ req.raw.on("close", () => {
547
+ clearInterval(heartbeat);
548
+ unsubscribe();
549
+ reply.raw.end();
550
+ });
551
+ });
552
+
553
+ app.get("/admin/benchmarks/capabilities", async (req, reply) => {
554
+ const ttlParam = Number((req.query as { ttlDays?: string } | undefined)?.ttlDays);
555
+ const ttlDays = Number.isFinite(ttlParam) && ttlParam > 0 ? Math.floor(ttlParam) : 7;
556
+ const data = await listCapabilitySnapshots(paths, ttlDays);
557
+ reply.send(toCapabilityMatrix(data));
558
+ });
559
+
560
+ app.get("/admin/benchmarks/capabilities/:modelId", async (req, reply) => {
561
+ const { modelId } = req.params as { modelId: string };
562
+ const ttlParam = Number((req.query as { ttlDays?: string } | undefined)?.ttlDays);
563
+ const ttlDays = Number.isFinite(ttlParam) && ttlParam > 0 ? Math.floor(ttlParam) : 7;
564
+ const model = await getCapabilitySnapshotByModel(paths, modelId, ttlDays);
565
+ if (!model) {
566
+ reply.code(404).send({ error: { message: "capability snapshot not found" } });
567
+ return;
568
+ }
569
+ reply.send(model);
570
+ });
571
+
572
+ app.get("/admin/capture/config", async (_req, reply) => {
573
+ const config = await getCaptureConfig(paths);
574
+ reply.send(config);
575
+ });
576
+
577
+ app.put(
578
+ "/admin/capture/config",
579
+ async (req: FastifyRequest<{ Body: { enabled?: boolean; retentionDays?: number; maxBytes?: number } }>, reply) => {
580
+ const next = await updateCaptureConfig(paths, req.body ?? {});
581
+ reply.send(next);
582
+ }
583
+ );
584
+
585
+ app.get("/admin/capture/records", async (req, reply) => {
586
+ const query = req.query as { limit?: string; offset?: string; date?: string; timeZone?: string } | undefined;
587
+ const parsed = Number(query?.limit);
588
+ const limit = Number.isFinite(parsed) ? Math.max(1, Math.min(50, Math.floor(parsed))) : 5;
589
+ const offsetParsed = Number(query?.offset);
590
+ const offset = Number.isFinite(offsetParsed) ? Math.max(0, Math.floor(offsetParsed)) : 0;
591
+ const timeZone = normalizeTimeZone(query?.timeZone);
592
+ const result = await listCaptureRecords(paths, {
593
+ limit,
594
+ offset,
595
+ date: typeof query?.date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(query.date) ? query.date : undefined,
596
+ timeZone,
597
+ });
598
+ reply.send({ object: "list", data: result.data, total: result.total });
599
+ });
600
+
601
+ app.get("/admin/capture/calendar", async (req, reply) => {
602
+ const query = req.query as { month?: string; timeZone?: string } | undefined;
603
+ const timeZone = normalizeTimeZone(query?.timeZone);
604
+ const month =
605
+ typeof query?.month === "string" && /^\d{4}-\d{2}$/.test(query.month)
606
+ ? query.month
607
+ : formatDateForTimeZone(new Date(), timeZone).slice(0, 7);
608
+ const days = await getCaptureCalendarMonth(paths, month, timeZone);
609
+ reply.send({ month, days });
610
+ });
611
+
612
+ app.get("/admin/capture/records/:id", async (req, reply) => {
613
+ const { id } = req.params as { id: string };
614
+ const record = await getCaptureRecordById(paths, id);
615
+ if (!record) {
616
+ reply.code(404).send({ error: { message: "capture record not found" } });
617
+ return;
618
+ }
619
+ reply.send(record);
620
+ });
621
+
622
+ app.get("/admin/capture/blobs/:hash", async (req, reply) => {
623
+ const { hash } = req.params as { hash: string };
624
+ const located = await findCaptureBlobPath(paths, hash);
625
+ if (!located) {
626
+ reply.code(404).send({ error: { message: "capture blob not found" } });
627
+ return;
628
+ }
629
+ const buffer = await fs.readFile(located.path);
630
+ reply.header("content-type", located.mime);
631
+ reply.send(buffer);
632
+ });
633
+ }
634
+
635
+ function isAuthorized(req: FastifyRequest, token?: string): boolean {
636
+ if (token) {
637
+ const header = req.headers.authorization ?? "";
638
+ return header === `Bearer ${token}`;
639
+ }
640
+ const remote = req.socket.remoteAddress ?? "";
641
+ return remote === "127.0.0.1" || remote === "::1";
642
+ }
643
+
644
+ function formatDateForTimeZone(value: Date, timeZone: string): string {
645
+ const formatter = new Intl.DateTimeFormat("en-CA", {
646
+ timeZone,
647
+ year: "numeric",
648
+ month: "2-digit",
649
+ day: "2-digit",
650
+ });
651
+ const parts = formatter.formatToParts(value);
652
+ const year = parts.find((part) => part.type === "year")?.value ?? "0000";
653
+ const month = parts.find((part) => part.type === "month")?.value ?? "00";
654
+ const day = parts.find((part) => part.type === "day")?.value ?? "00";
655
+ return `${year}-${month}-${day}`;
656
+ }
657
+
658
+ function normalizeTimeZone(input: string | undefined): string {
659
+ if (!input) return "UTC";
660
+ try {
661
+ new Intl.DateTimeFormat("en-US", { timeZone: input });
662
+ return input;
663
+ } catch {
664
+ return "UTC";
665
+ }
666
+ }