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,293 @@
1
+ import path from "path";
2
+ import { StoragePaths } from "../storage/files";
3
+ import { getMediaEntry, getMediaPath } from "../storage/imageCache";
4
+
5
+ export interface MessageMediaScan {
6
+ hasImage: boolean;
7
+ hasAudio: boolean;
8
+ }
9
+
10
+ export function scanMessageModalities(messages: unknown): MessageMediaScan {
11
+ const result: MessageMediaScan = { hasImage: false, hasAudio: false };
12
+ if (!Array.isArray(messages)) {
13
+ return result;
14
+ }
15
+
16
+ for (const message of messages) {
17
+ if (!message || typeof message !== "object") {
18
+ continue;
19
+ }
20
+ const content = (message as { content?: unknown }).content;
21
+ if (!Array.isArray(content)) {
22
+ continue;
23
+ }
24
+ for (const part of content) {
25
+ if (!part || typeof part !== "object") {
26
+ continue;
27
+ }
28
+ const type = (part as { type?: unknown }).type;
29
+ if (type === "image_url" || type === "input_image" || type === "image") {
30
+ result.hasImage = true;
31
+ }
32
+ if (type === "input_audio" || type === "audio") {
33
+ result.hasAudio = true;
34
+ }
35
+ }
36
+ }
37
+
38
+ return result;
39
+ }
40
+
41
+ export async function normalizeMessagesForUpstream(
42
+ paths: StoragePaths,
43
+ messages: unknown
44
+ ): Promise<unknown> {
45
+ if (!Array.isArray(messages)) {
46
+ return messages;
47
+ }
48
+
49
+ const normalized: unknown[] = [];
50
+ for (const message of messages) {
51
+ if (!message || typeof message !== "object") {
52
+ normalized.push(message);
53
+ continue;
54
+ }
55
+
56
+ const nextMessage: Record<string, unknown> = { ...(message as Record<string, unknown>) };
57
+ const content = nextMessage.content;
58
+ if (!Array.isArray(content)) {
59
+ normalized.push(nextMessage);
60
+ continue;
61
+ }
62
+
63
+ const nextContent: unknown[] = [];
64
+ for (const rawPart of content) {
65
+ if (!rawPart || typeof rawPart !== "object") {
66
+ nextContent.push(rawPart);
67
+ continue;
68
+ }
69
+ const part = { ...(rawPart as Record<string, unknown>) };
70
+ const type = part.type;
71
+
72
+ if (type === "video") {
73
+ throw invalidRequestError("Video content is not supported in v1 omni mode.");
74
+ }
75
+
76
+ if (type === "image" && typeof part.image === "string") {
77
+ nextContent.push({ type: "image_url", image_url: { url: part.image } });
78
+ continue;
79
+ }
80
+
81
+ if (type === "image_url") {
82
+ nextContent.push(await normalizeImageUrlPart(paths, part));
83
+ continue;
84
+ }
85
+
86
+ if (type === "audio" && typeof part.audio === "string") {
87
+ nextContent.push(await normalizeAudioValue(paths, part.audio));
88
+ continue;
89
+ }
90
+
91
+ if (type === "input_audio") {
92
+ nextContent.push(await normalizeInputAudioPart(paths, part));
93
+ continue;
94
+ }
95
+
96
+ nextContent.push(part);
97
+ }
98
+
99
+ nextMessage.content = nextContent;
100
+ normalized.push(nextMessage);
101
+ }
102
+
103
+ return normalized;
104
+ }
105
+
106
+ async function normalizeImageUrlPart(
107
+ paths: StoragePaths,
108
+ part: Record<string, unknown>
109
+ ): Promise<Record<string, unknown>> {
110
+ const imageUrlObject = (part.image_url ?? {}) as { url?: unknown };
111
+ const value = imageUrlObject.url;
112
+ if (typeof value !== "string" || value.length === 0) {
113
+ return part;
114
+ }
115
+
116
+ if (value.startsWith("data:")) {
117
+ return part;
118
+ }
119
+
120
+ const hash = extractLocalHash(value);
121
+ if (!hash) {
122
+ return part;
123
+ }
124
+
125
+ const mediaPath = await getMediaPath(paths, hash);
126
+ const mediaEntry = await getMediaEntry(paths, hash);
127
+ if (!mediaPath || !mediaEntry) {
128
+ throw invalidRequestError("Referenced image not found in cache.");
129
+ }
130
+ const file = await import("fs/promises");
131
+ const buffer = await file.readFile(mediaPath);
132
+ const dataUrl = `data:${mediaEntry.mimeType};base64,${buffer.toString("base64")}`;
133
+
134
+ return {
135
+ ...part,
136
+ type: "image_url",
137
+ image_url: {
138
+ ...imageUrlObject,
139
+ url: dataUrl,
140
+ },
141
+ };
142
+ }
143
+
144
+ async function normalizeInputAudioPart(
145
+ paths: StoragePaths,
146
+ part: Record<string, unknown>
147
+ ): Promise<Record<string, unknown>> {
148
+ const inputAudio = (part.input_audio ?? {}) as Record<string, unknown>;
149
+ const data = inputAudio.data;
150
+ const url = inputAudio.url;
151
+
152
+ if (typeof data === "string" && data.length > 0) {
153
+ return {
154
+ ...part,
155
+ type: "input_audio",
156
+ input_audio: {
157
+ ...inputAudio,
158
+ },
159
+ };
160
+ }
161
+
162
+ if (typeof url === "string" && url.length > 0) {
163
+ const resolved = await resolveLocalMediaUrl(paths, url);
164
+ return {
165
+ ...part,
166
+ type: "input_audio",
167
+ input_audio: resolved,
168
+ };
169
+ }
170
+
171
+ throw invalidRequestError("input_audio requires either data or a local media url.");
172
+ }
173
+
174
+ async function normalizeAudioValue(paths: StoragePaths, value: string): Promise<Record<string, unknown>> {
175
+ if (value.startsWith("data:")) {
176
+ const parsed = parseDataUrl(value);
177
+ return {
178
+ type: "input_audio",
179
+ input_audio: {
180
+ data: parsed.base64,
181
+ format: parsed.format,
182
+ },
183
+ };
184
+ }
185
+
186
+ if (looksLikeBase64(value)) {
187
+ return {
188
+ type: "input_audio",
189
+ input_audio: {
190
+ data: value,
191
+ },
192
+ };
193
+ }
194
+
195
+ const resolved = await resolveLocalMediaUrl(paths, value);
196
+ return {
197
+ type: "input_audio",
198
+ input_audio: resolved,
199
+ };
200
+ }
201
+
202
+ async function resolveLocalMediaUrl(
203
+ paths: StoragePaths,
204
+ url: string
205
+ ): Promise<{ data: string; format?: string }> {
206
+ const hash = extractLocalHash(url);
207
+ if (!hash) {
208
+ throw invalidRequestError("Only local /admin/media or /admin/images URLs are allowed for input_audio.");
209
+ }
210
+
211
+ const mediaPath = await getMediaPath(paths, hash);
212
+ const mediaEntry = await getMediaEntry(paths, hash);
213
+ if (!mediaPath || !mediaEntry) {
214
+ throw invalidRequestError("Referenced media not found in cache.");
215
+ }
216
+
217
+ const file = await import("fs/promises");
218
+ const buffer = await file.readFile(mediaPath);
219
+ return {
220
+ data: buffer.toString("base64"),
221
+ format: audioFormatFromMime(mediaEntry.mimeType, mediaPath),
222
+ };
223
+ }
224
+
225
+ function extractLocalHash(url: string): string | null {
226
+ const normalized = normalizeLocalUrl(url);
227
+ if (!normalized) {
228
+ return null;
229
+ }
230
+ const mediaMatch = normalized.match(/^\/admin\/(media|images)\/([a-f0-9]{16})$/i);
231
+ if (!mediaMatch) {
232
+ return null;
233
+ }
234
+ return mediaMatch[2];
235
+ }
236
+
237
+ function normalizeLocalUrl(url: string): string | null {
238
+ if (url.startsWith("/")) {
239
+ return url;
240
+ }
241
+
242
+ try {
243
+ const parsed = new URL(url);
244
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
245
+ return null;
246
+ }
247
+ if (!["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
248
+ return null;
249
+ }
250
+ return parsed.pathname;
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ function parseDataUrl(value: string): { base64: string; format?: string } {
257
+ const match = value.match(/^data:([^;]+);base64,(.+)$/i);
258
+ if (!match) {
259
+ throw invalidRequestError("Invalid data URL for audio input.");
260
+ }
261
+ return {
262
+ base64: match[2].replace(/\s+/g, ""),
263
+ format: audioFormatFromMime(match[1]),
264
+ };
265
+ }
266
+
267
+ function audioFormatFromMime(mimeType: string, filePath?: string): string | undefined {
268
+ const lower = mimeType.toLowerCase();
269
+ if (lower.includes("wav")) return "wav";
270
+ if (lower.includes("mpeg") || lower.includes("mp3")) return "mp3";
271
+ if (lower.includes("ogg")) return "ogg";
272
+ if (lower.includes("webm")) return "webm";
273
+ if (lower.includes("mp4") || lower.includes("m4a")) return "m4a";
274
+ if (filePath) {
275
+ const ext = path.extname(filePath).slice(1).toLowerCase();
276
+ if (ext) return ext;
277
+ }
278
+ return undefined;
279
+ }
280
+
281
+ function looksLikeBase64(value: string): boolean {
282
+ if (value.length < 32) {
283
+ return false;
284
+ }
285
+ return /^[A-Za-z0-9+/=\s]+$/.test(value);
286
+ }
287
+
288
+ function invalidRequestError(message: string): Error & { type: string; retryable: boolean } {
289
+ const error = new Error(message) as Error & { type: string; retryable: boolean };
290
+ error.type = "invalid_request";
291
+ error.retryable = false;
292
+ return error;
293
+ }
@@ -0,0 +1,161 @@
1
+ import {
2
+ EndpointType,
3
+ ModelCapabilities,
4
+ ModelMapping,
5
+ ModelModality,
6
+ } from "../types";
7
+
8
+ export interface CapabilitiesRequirements {
9
+ requiredInput?: ModelModality[];
10
+ requiredOutput?: ModelModality[];
11
+ }
12
+
13
+ export function resolveCapabilities(
14
+ mapping: ModelMapping,
15
+ endpointType: EndpointType,
16
+ upstreamCaps?: ModelCapabilities
17
+ ): ModelCapabilities {
18
+ if (mapping.capabilities) {
19
+ return normalizeCapabilities(mapping.capabilities, "configured");
20
+ }
21
+ if (upstreamCaps) {
22
+ return normalizeCapabilities(upstreamCaps, "inferred");
23
+ }
24
+
25
+ const inferred = inferCapabilities(mapping.publicName, endpointType);
26
+ warnInference(mapping.publicName, endpointType, inferred);
27
+ return normalizeCapabilities(inferred, "inferred");
28
+ }
29
+
30
+ export function inferCapabilities(
31
+ modelName: string,
32
+ endpointType: EndpointType
33
+ ): ModelCapabilities {
34
+ const name = modelName.toLowerCase();
35
+
36
+ if (endpointType === "embedding") {
37
+ return { input: ["text"], output: ["embedding"] };
38
+ }
39
+
40
+ if (endpointType === "diffusion") {
41
+ return { input: ["text"], output: ["image"] };
42
+ }
43
+
44
+ if (endpointType === "audio") {
45
+ if (isTtsModelName(name)) {
46
+ return { input: ["text"], output: ["audio"] };
47
+ }
48
+ return { input: ["audio"], output: ["text"] };
49
+ }
50
+
51
+ if (endpointType === "video") {
52
+ if (isImageToVideoModelName(name)) {
53
+ return { input: ["text", "image"], output: ["video"] };
54
+ }
55
+ return { input: ["text"], output: ["video"] };
56
+ }
57
+
58
+ if (isVisionModelName(name)) {
59
+ return { input: ["text", "image"], output: ["text"], supportsTools: true, supportsStreaming: true };
60
+ }
61
+
62
+ return { input: ["text"], output: ["text"], supportsTools: true, supportsStreaming: true };
63
+ }
64
+
65
+ export function supportsRequirements(
66
+ capabilities: ModelCapabilities,
67
+ requirements?: CapabilitiesRequirements
68
+ ): boolean {
69
+ if (!requirements) {
70
+ return true;
71
+ }
72
+
73
+ if (requirements.requiredInput && requirements.requiredInput.length > 0) {
74
+ for (const modality of requirements.requiredInput) {
75
+ if (!capabilities.input.includes(modality)) {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+
81
+ if (requirements.requiredOutput && requirements.requiredOutput.length > 0) {
82
+ for (const modality of requirements.requiredOutput) {
83
+ if (!capabilities.output.includes(modality)) {
84
+ return false;
85
+ }
86
+ }
87
+ }
88
+
89
+ return true;
90
+ }
91
+
92
+ function normalizeCapabilities(
93
+ capabilities: ModelCapabilities,
94
+ source: "configured" | "inferred"
95
+ ): ModelCapabilities {
96
+ return {
97
+ input: normalizeModalities(capabilities.input),
98
+ output: normalizeModalities(capabilities.output),
99
+ supportsTools: capabilities.supportsTools,
100
+ supportsStreaming: capabilities.supportsStreaming,
101
+ source,
102
+ };
103
+ }
104
+
105
+ function normalizeModalities(modalities: ModelModality[]): ModelModality[] {
106
+ const allowed: ModelModality[] = ["text", "image", "audio", "embedding", "video"];
107
+ const unique = new Set<ModelModality>();
108
+
109
+ for (const modality of modalities) {
110
+ if (allowed.includes(modality)) {
111
+ unique.add(modality);
112
+ }
113
+ }
114
+
115
+ return allowed.filter((modality) => unique.has(modality));
116
+ }
117
+
118
+ function isTtsModelName(name: string): boolean {
119
+ return (
120
+ name.includes("tts") ||
121
+ name.includes("speech") ||
122
+ name.includes("voice") ||
123
+ name.includes("audio-gen")
124
+ );
125
+ }
126
+
127
+ function isVisionModelName(name: string): boolean {
128
+ return (
129
+ name.includes("vision") ||
130
+ name.includes("vl") ||
131
+ name.includes("omni") ||
132
+ name.includes("multimodal")
133
+ );
134
+ }
135
+
136
+ function isImageToVideoModelName(name: string): boolean {
137
+ return (
138
+ name.includes("i2v") ||
139
+ name.includes("image-to-video") ||
140
+ name.includes("img2vid") ||
141
+ name.includes("kf2v")
142
+ );
143
+ }
144
+
145
+ const capabilityInferenceWarnings = new Set<string>();
146
+
147
+ function warnInference(
148
+ modelName: string,
149
+ endpointType: EndpointType,
150
+ capabilities: ModelCapabilities
151
+ ): void {
152
+ const key = `${endpointType}:${modelName}`;
153
+ if (capabilityInferenceWarnings.has(key)) {
154
+ return;
155
+ }
156
+ capabilityInferenceWarnings.add(key);
157
+ console.warn(
158
+ `[waypoi] Inferred capabilities for model '${modelName}' on ${endpointType}: ` +
159
+ `${capabilities.input.join("+")}->${capabilities.output.join("+")}`
160
+ );
161
+ }
@@ -0,0 +1,203 @@
1
+ import { Agent, request } from "undici";
2
+ import { ModelCapabilities, ModelMapping, ModelModality } from "../types";
3
+ import { ProviderAuthConfig } from "../providers/types";
4
+
5
+ export interface EndpointInfo {
6
+ baseUrl: string;
7
+ apiKey?: string;
8
+ insecureTls: boolean;
9
+ auth?: ProviderAuthConfig;
10
+ }
11
+
12
+ export async function resolveModelMappings(
13
+ endpoint: EndpointInfo,
14
+ mappings: ModelMapping[]
15
+ ): Promise<ModelMapping[]> {
16
+ let models: UpstreamModelInfo[] | null = null;
17
+ try {
18
+ models = await discoverUpstreamModels(endpoint);
19
+ } catch {
20
+ models = null;
21
+ }
22
+ if (!models || models.length === 0) {
23
+ return mappings;
24
+ }
25
+
26
+ // If exactly one model, use it as the upstream for all mappings
27
+ // that don't already have an explicit upstream different from the public name
28
+ if (models.length === 1) {
29
+ const sole = models[0];
30
+ console.log(`[model-discovery] Single model found: ${sole.id}`);
31
+ return mappings.map((mapping) => {
32
+ // If user specified explicit upstream (public=upstream format), keep it
33
+ // Otherwise, use the discovered model as upstream
34
+ if (mapping.publicName !== mapping.upstreamModel) {
35
+ // User specified explicit mapping, keep it
36
+ return mapping;
37
+ }
38
+ // Public and upstream are the same (user just gave public name)
39
+ // Replace upstream with discovered model
40
+ console.log(`[model-discovery] Mapping ${mapping.publicName} -> ${sole.id}`);
41
+ return {
42
+ ...mapping,
43
+ upstreamModel: sole.id,
44
+ capabilities: mapping.capabilities ?? sole.capabilities,
45
+ };
46
+ });
47
+ }
48
+
49
+ // Multiple models - check if any mapping's upstream matches available models
50
+ console.log(
51
+ `[model-discovery] ${models.length} models found: ${models
52
+ .slice(0, 5)
53
+ .map((model) => model.id)
54
+ .join(", ")}${models.length > 5 ? "..." : ""}`
55
+ );
56
+ const byId = new Map(models.map((model) => [model.id, model]));
57
+ return mappings.map((mapping) => {
58
+ if (mapping.capabilities) {
59
+ return mapping;
60
+ }
61
+ const matched = byId.get(mapping.upstreamModel) ?? byId.get(mapping.publicName);
62
+ if (!matched?.capabilities) {
63
+ return mapping;
64
+ }
65
+ return {
66
+ ...mapping,
67
+ capabilities: matched.capabilities,
68
+ };
69
+ });
70
+ }
71
+
72
+ interface UpstreamModelInfo {
73
+ id: string;
74
+ capabilities?: ModelCapabilities;
75
+ }
76
+
77
+ export async function discoverUpstreamModels(endpoint: EndpointInfo): Promise<UpstreamModelInfo[]> {
78
+ const dispatcher = endpoint.insecureTls
79
+ ? new Agent({ connect: { rejectUnauthorized: false } })
80
+ : undefined;
81
+ const { url, headers } = buildDiscoveryRequest(endpoint);
82
+ const response = await request(url, {
83
+ method: "GET",
84
+ headersTimeout: 3000,
85
+ bodyTimeout: 3000,
86
+ dispatcher,
87
+ headers,
88
+ });
89
+ const body = (await readJson(response.body)) as {
90
+ data?: Array<{
91
+ id?: string;
92
+ input_modalities?: string[];
93
+ output_modalities?: string[];
94
+ capabilities?: { input?: string[]; output?: string[]; supportsTools?: boolean; supportsStreaming?: boolean };
95
+ }>;
96
+ } | null;
97
+ response.body.resume();
98
+ if (response.statusCode < 200 || response.statusCode >= 300) {
99
+ throw new Error(`model discovery failed with status ${response.statusCode}`);
100
+ }
101
+ const list = Array.isArray(body?.data) ? body.data : [];
102
+ const models: UpstreamModelInfo[] = [];
103
+ for (const item of list) {
104
+ if (!item.id) {
105
+ continue;
106
+ }
107
+ const modelInfo: UpstreamModelInfo = { id: item.id };
108
+ const capabilities = extractCapabilities(item);
109
+ if (capabilities) {
110
+ modelInfo.capabilities = capabilities;
111
+ }
112
+ models.push(modelInfo);
113
+ }
114
+ return models;
115
+ }
116
+
117
+ function buildDiscoveryRequest(endpoint: EndpointInfo): { url: string; headers: Record<string, string> } {
118
+ const authType = endpoint.auth?.type ?? "bearer";
119
+ const headers: Record<string, string> = {};
120
+ const url = new URL(buildModelListUrl(endpoint.baseUrl));
121
+ const apiKey = endpoint.apiKey?.trim();
122
+
123
+ if (apiKey && authType === "query") {
124
+ const keyParam = endpoint.auth?.keyParam?.trim() || "api_key";
125
+ url.searchParams.set(keyParam, apiKey);
126
+ } else if (apiKey && authType === "header") {
127
+ const headerName = endpoint.auth?.headerName?.trim() || endpoint.auth?.keyParam?.trim() || "x-api-key";
128
+ const prefix = endpoint.auth?.keyPrefix?.trim();
129
+ headers[headerName] = prefix ? `${prefix} ${apiKey}` : apiKey;
130
+ } else if (apiKey && authType !== "none") {
131
+ headers.authorization = `Bearer ${apiKey}`;
132
+ }
133
+
134
+ return { url: url.toString(), headers };
135
+ }
136
+
137
+ function buildModelListUrl(baseUrl: string): string {
138
+ const parsed = new URL(baseUrl);
139
+ const pathname = parsed.pathname.replace(/\/+$/, "");
140
+ if (!pathname) {
141
+ parsed.pathname = "/v1/models";
142
+ } else if (pathname.endsWith("/v1")) {
143
+ parsed.pathname = `${pathname}/models`;
144
+ } else {
145
+ parsed.pathname = `${pathname}/v1/models`;
146
+ }
147
+ return parsed.toString();
148
+ }
149
+
150
+ function extractCapabilities(item: {
151
+ input_modalities?: string[];
152
+ output_modalities?: string[];
153
+ capabilities?: { input?: string[]; output?: string[]; supportsTools?: boolean; supportsStreaming?: boolean };
154
+ }): ModelCapabilities | undefined {
155
+ const fromCapabilities = item.capabilities;
156
+ if (fromCapabilities?.input && fromCapabilities?.output) {
157
+ const input = normalizeModalities(fromCapabilities.input);
158
+ const output = normalizeModalities(fromCapabilities.output);
159
+ if (input.length > 0 && output.length > 0) {
160
+ return {
161
+ input,
162
+ output,
163
+ supportsTools: fromCapabilities.supportsTools,
164
+ supportsStreaming: fromCapabilities.supportsStreaming,
165
+ source: "inferred",
166
+ };
167
+ }
168
+ }
169
+
170
+ const input = normalizeModalities(item.input_modalities ?? []);
171
+ const output = normalizeModalities(item.output_modalities ?? []);
172
+ if (input.length > 0 && output.length > 0) {
173
+ return { input, output, source: "inferred" };
174
+ }
175
+
176
+ return undefined;
177
+ }
178
+
179
+ function normalizeModalities(values: string[]): ModelModality[] {
180
+ const normalized = new Set<ModelModality>();
181
+ for (const value of values) {
182
+ const lower = value.toLowerCase();
183
+ if (lower === "text" || lower === "image" || lower === "audio" || lower === "embedding") {
184
+ normalized.add(lower);
185
+ }
186
+ }
187
+ return Array.from(normalized);
188
+ }
189
+
190
+ async function readJson(stream: NodeJS.ReadableStream): Promise<unknown> {
191
+ const chunks: Buffer[] = [];
192
+ for await (const chunk of stream) {
193
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
194
+ }
195
+ if (chunks.length === 0) {
196
+ return null;
197
+ }
198
+ try {
199
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
@@ -0,0 +1,25 @@
1
+ import { StoragePaths } from "../storage/files";
2
+ import { runCaptureRetention } from "../storage/captureRepository";
3
+
4
+ let retentionTimer: NodeJS.Timeout | null = null;
5
+
6
+ export function startCaptureRetentionWorker(paths: StoragePaths): void {
7
+ const run = async () => {
8
+ try {
9
+ await runCaptureRetention(paths);
10
+ } catch {
11
+ // ignore background errors
12
+ }
13
+ };
14
+
15
+ retentionTimer = setInterval(run, 10 * 60 * 1000);
16
+ retentionTimer.unref();
17
+ void run();
18
+ }
19
+
20
+ export function stopCaptureRetentionWorker(): void {
21
+ if (retentionTimer) {
22
+ clearInterval(retentionTimer);
23
+ retentionTimer = null;
24
+ }
25
+ }