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,370 @@
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.resolveSessionsDir = resolveSessionsDir;
7
+ exports.listSessions = listSessions;
8
+ exports.getSession = getSession;
9
+ exports.createSession = createSession;
10
+ exports.updateSession = updateSession;
11
+ exports.deleteSession = deleteSession;
12
+ exports.addMessage = addMessage;
13
+ exports.appendMessageContent = appendMessageContent;
14
+ const fs_1 = require("fs");
15
+ const path_1 = __importDefault(require("path"));
16
+ const crypto_1 = __importDefault(require("crypto"));
17
+ const files_1 = require("./files");
18
+ const imageCache_1 = require("./imageCache");
19
+ /**
20
+ * Session Repository
21
+ *
22
+ * Manages chat sessions for the playground UI.
23
+ * Sessions are stored as JSON files in ~/.config/waypoi/sessions/
24
+ */
25
+ function resolveSessionsDir(paths) {
26
+ return path_1.default.join(paths.baseDir, "sessions");
27
+ }
28
+ async function ensureSessionsDir(paths) {
29
+ await (0, files_1.ensureStorageDir)(paths);
30
+ const sessionsDir = resolveSessionsDir(paths);
31
+ await fs_1.promises.mkdir(sessionsDir, { recursive: true });
32
+ }
33
+ function sessionFilePath(paths, sessionId) {
34
+ return path_1.default.join(resolveSessionsDir(paths), `${sessionId}.json`);
35
+ }
36
+ async function listSessions(paths) {
37
+ await ensureSessionsDir(paths);
38
+ const sessionsDir = resolveSessionsDir(paths);
39
+ try {
40
+ const files = await fs_1.promises.readdir(sessionsDir);
41
+ const sessions = [];
42
+ for (const file of files) {
43
+ if (!file.endsWith(".json"))
44
+ continue;
45
+ try {
46
+ const filePath = path_1.default.join(sessionsDir, file);
47
+ const raw = await fs_1.promises.readFile(filePath, "utf8");
48
+ let session = parseSession(JSON.parse(raw));
49
+ const migrated = await migrateSessionMediaRefs(paths, session);
50
+ if (migrated.changed) {
51
+ session = migrated.session;
52
+ await saveSession(paths, session);
53
+ }
54
+ sessions.push(session);
55
+ }
56
+ catch {
57
+ // Skip malformed session files
58
+ }
59
+ }
60
+ // Sort by updatedAt descending (most recent first)
61
+ return sessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
62
+ }
63
+ catch (error) {
64
+ if (error.code === "ENOENT") {
65
+ return [];
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+ async function getSession(paths, sessionId) {
71
+ await ensureSessionsDir(paths);
72
+ const filePath = sessionFilePath(paths, sessionId);
73
+ try {
74
+ const raw = await fs_1.promises.readFile(filePath, "utf8");
75
+ let session = parseSession(JSON.parse(raw));
76
+ const migrated = await migrateSessionMediaRefs(paths, session);
77
+ if (migrated.changed) {
78
+ session = migrated.session;
79
+ await saveSession(paths, session);
80
+ }
81
+ return session;
82
+ }
83
+ catch (error) {
84
+ if (error.code === "ENOENT") {
85
+ return null;
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ async function createSession(paths, input) {
91
+ await ensureSessionsDir(paths);
92
+ const now = new Date();
93
+ const session = {
94
+ id: crypto_1.default.randomUUID(),
95
+ name: input.name ?? `Session ${now.toLocaleDateString()}`,
96
+ model: input.model,
97
+ titleStatus: input.name ? "manual" : "pending",
98
+ titleUpdatedAt: now,
99
+ storageVersion: 2,
100
+ messages: [],
101
+ createdAt: now,
102
+ updatedAt: now,
103
+ };
104
+ await saveSession(paths, session);
105
+ return session;
106
+ }
107
+ async function updateSession(paths, sessionId, patch) {
108
+ const session = await getSession(paths, sessionId);
109
+ if (!session)
110
+ return null;
111
+ const titleStatus = patch.titleStatus ??
112
+ (patch.name !== undefined ? "manual" : session.titleStatus);
113
+ const titleUpdatedAt = patch.titleUpdatedAt ??
114
+ (patch.name !== undefined || patch.titleStatus !== undefined ? new Date() : session.titleUpdatedAt);
115
+ const updated = {
116
+ ...session,
117
+ ...patch,
118
+ titleStatus,
119
+ titleUpdatedAt,
120
+ updatedAt: new Date(),
121
+ };
122
+ await saveSession(paths, updated);
123
+ return updated;
124
+ }
125
+ async function deleteSession(paths, sessionId) {
126
+ const filePath = sessionFilePath(paths, sessionId);
127
+ try {
128
+ await (0, imageCache_1.unmarkSessionMediaReferences)(paths, sessionId);
129
+ await fs_1.promises.unlink(filePath);
130
+ // Free any media that is now unreferenced
131
+ await (0, imageCache_1.cleanOrphanedMedia)(paths);
132
+ return true;
133
+ }
134
+ catch (error) {
135
+ if (error.code === "ENOENT") {
136
+ return false;
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+ async function addMessage(paths, sessionId, message) {
142
+ const session = await getSession(paths, sessionId);
143
+ if (!session)
144
+ return null;
145
+ const normalizedMessage = await normalizeMessageMediaRefs(paths, message);
146
+ const newMessage = {
147
+ ...normalizedMessage,
148
+ id: crypto_1.default.randomUUID(),
149
+ createdAt: new Date(),
150
+ };
151
+ session.messages.push(newMessage);
152
+ session.updatedAt = new Date();
153
+ await saveSession(paths, session);
154
+ return newMessage;
155
+ }
156
+ async function appendMessageContent(paths, sessionId, messageId, content) {
157
+ const session = await getSession(paths, sessionId);
158
+ if (!session)
159
+ return false;
160
+ const message = session.messages.find((m) => m.id === messageId);
161
+ if (!message)
162
+ return false;
163
+ message.content = (message.content ?? "") + content;
164
+ session.updatedAt = new Date();
165
+ await saveSession(paths, session);
166
+ return true;
167
+ }
168
+ async function saveSession(paths, session) {
169
+ const filePath = sessionFilePath(paths, session.id);
170
+ const json = JSON.stringify(session, null, 2);
171
+ await fs_1.promises.writeFile(filePath, json, "utf8");
172
+ await (0, imageCache_1.syncSessionMediaReferences)(paths, session.id, extractMediaHashesFromSession(session));
173
+ }
174
+ function parseSession(raw) {
175
+ const session = {
176
+ ...raw,
177
+ storageVersion: typeof raw.storageVersion === "number" ? raw.storageVersion : 1,
178
+ titleStatus: raw.titleStatus === "pending" ||
179
+ raw.titleStatus === "generated" ||
180
+ raw.titleStatus === "manual" ||
181
+ raw.titleStatus === "failed"
182
+ ? raw.titleStatus
183
+ : undefined,
184
+ titleUpdatedAt: raw.titleUpdatedAt ? new Date(raw.titleUpdatedAt) : undefined,
185
+ createdAt: new Date(raw.createdAt),
186
+ updatedAt: new Date(raw.updatedAt),
187
+ messages: Array.isArray(raw.messages)
188
+ ? raw.messages.map((message) => ({
189
+ ...message,
190
+ createdAt: parseMessageDate(message),
191
+ }))
192
+ : [],
193
+ };
194
+ return session;
195
+ }
196
+ function parseMessageDate(message) {
197
+ if (message.createdAt) {
198
+ return new Date(message.createdAt);
199
+ }
200
+ if (typeof message.timestamp === "string") {
201
+ return new Date(message.timestamp);
202
+ }
203
+ return new Date();
204
+ }
205
+ async function migrateSessionMediaRefs(paths, session) {
206
+ let changed = false;
207
+ const migratedMessages = [];
208
+ for (const message of session.messages) {
209
+ const normalized = await normalizeMessageMediaRefs(paths, message);
210
+ if (!changed && JSON.stringify(normalized) !== JSON.stringify(message)) {
211
+ changed = true;
212
+ }
213
+ migratedMessages.push({
214
+ ...normalized,
215
+ id: message.id,
216
+ createdAt: message.createdAt,
217
+ });
218
+ }
219
+ const nextStorageVersion = session.storageVersion >= 2 ? session.storageVersion : 2;
220
+ if (nextStorageVersion !== session.storageVersion) {
221
+ changed = true;
222
+ }
223
+ if (!changed) {
224
+ return { session, changed: false };
225
+ }
226
+ return {
227
+ changed: true,
228
+ session: {
229
+ ...session,
230
+ storageVersion: nextStorageVersion,
231
+ messages: migratedMessages,
232
+ updatedAt: new Date(),
233
+ },
234
+ };
235
+ }
236
+ async function normalizeMessageMediaRefs(paths, message) {
237
+ const next = {
238
+ role: message.role,
239
+ content: message.content ?? "",
240
+ name: message.name,
241
+ tool_calls: message.tool_calls,
242
+ tool_call_id: message.tool_call_id,
243
+ images: message.images,
244
+ model: message.model,
245
+ };
246
+ // Normalize convenience image list
247
+ if (Array.isArray(next.images)) {
248
+ const normalizedImages = [];
249
+ for (const value of next.images) {
250
+ const cachedUrl = await normalizeImageRefToLocalUrl(paths, value);
251
+ normalizedImages.push(cachedUrl);
252
+ }
253
+ next.images = normalizedImages;
254
+ }
255
+ // Normalize image_url parts in content
256
+ if (Array.isArray(next.content)) {
257
+ const normalizedContent = [];
258
+ for (const part of next.content) {
259
+ if (part &&
260
+ typeof part === "object" &&
261
+ part.type === "image_url" &&
262
+ part.image_url &&
263
+ typeof part.image_url.url === "string") {
264
+ const normalizedUrl = await normalizeImageRefToLocalUrl(paths, part.image_url.url);
265
+ normalizedContent.push({
266
+ ...part,
267
+ image_url: {
268
+ ...part.image_url,
269
+ url: normalizedUrl,
270
+ },
271
+ });
272
+ }
273
+ else {
274
+ normalizedContent.push(part);
275
+ }
276
+ }
277
+ next.content = normalizedContent;
278
+ }
279
+ return next;
280
+ }
281
+ /**
282
+ * Normalize an image reference to a stable /admin/media/{hash} URL.
283
+ * If conversion fails, the ORIGINAL reference is returned so images are never
284
+ * silently dropped from sessions. A partial failure is far better than data loss.
285
+ */
286
+ async function normalizeImageRefToLocalUrl(paths, value) {
287
+ const trimmed = value.trim();
288
+ if (!trimmed)
289
+ return value;
290
+ const localHash = extractLocalMediaHash(trimmed);
291
+ if (localHash) {
292
+ return `/admin/media/${localHash}`;
293
+ }
294
+ if (/^[a-f0-9]{16}$/i.test(trimmed)) {
295
+ return `/admin/media/${trimmed.toLowerCase()}`;
296
+ }
297
+ if (trimmed.startsWith("data:image/") || trimmed.startsWith("data:audio/")) {
298
+ try {
299
+ const cached = await (0, imageCache_1.storeMedia)(paths, trimmed);
300
+ return `/admin/media/${cached.hash}`;
301
+ }
302
+ catch (err) {
303
+ console.error(`[waypoi] Failed to cache media ref (preserving original): ${err.message}`);
304
+ return value; // preserve — don't discard
305
+ }
306
+ }
307
+ if (/^https?:\/\//i.test(trimmed)) {
308
+ try {
309
+ const response = await fetch(trimmed, { signal: AbortSignal.timeout(10_000) });
310
+ if (!response.ok) {
311
+ console.error(`[waypoi] Failed to fetch remote image (${response.status}), preserving URL: ${trimmed.slice(0, 80)}`);
312
+ return value; // preserve the URL — might resolve later
313
+ }
314
+ const contentType = response.headers.get("content-type") ?? undefined;
315
+ const buffer = Buffer.from(await response.arrayBuffer());
316
+ const cached = await (0, imageCache_1.storeMedia)(paths, buffer, { mimeType: contentType });
317
+ return `/admin/media/${cached.hash}`;
318
+ }
319
+ catch (err) {
320
+ console.error(`[waypoi] Failed to fetch/cache remote image (preserving URL): ${err.message}`);
321
+ return value; // preserve — might be temporarily unreachable
322
+ }
323
+ }
324
+ // Unknown format — preserve as-is
325
+ return value;
326
+ }
327
+ function extractLocalMediaHash(value) {
328
+ if (value.startsWith("/")) {
329
+ const match = value.match(/^\/admin\/(?:media|images)\/([a-f0-9]{16})$/i);
330
+ return match ? match[1].toLowerCase() : null;
331
+ }
332
+ try {
333
+ const parsed = new URL(value);
334
+ if (!["http:", "https:"].includes(parsed.protocol))
335
+ return null;
336
+ if (!["localhost", "127.0.0.1", "::1"].includes(parsed.hostname))
337
+ return null;
338
+ const match = parsed.pathname.match(/^\/admin\/(?:media|images)\/([a-f0-9]{16})$/i);
339
+ return match ? match[1].toLowerCase() : null;
340
+ }
341
+ catch {
342
+ return null;
343
+ }
344
+ }
345
+ function extractMediaHashesFromSession(session) {
346
+ const hashes = new Set();
347
+ for (const message of session.messages) {
348
+ if (Array.isArray(message.images)) {
349
+ for (const imageRef of message.images) {
350
+ const hash = extractLocalMediaHash(imageRef);
351
+ if (hash)
352
+ hashes.add(hash);
353
+ }
354
+ }
355
+ if (Array.isArray(message.content)) {
356
+ for (const part of message.content) {
357
+ if (part &&
358
+ typeof part === "object" &&
359
+ part.type === "image_url" &&
360
+ part.image_url &&
361
+ typeof part.image_url.url === "string") {
362
+ const hash = extractLocalMediaHash(part.image_url.url);
363
+ if (hash)
364
+ hashes.add(hash);
365
+ }
366
+ }
367
+ }
368
+ }
369
+ return Array.from(hashes);
370
+ }
@@ -0,0 +1,204 @@
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.resolveStatsDir = resolveStatsDir;
7
+ exports.ensureStatsDir = ensureStatsDir;
8
+ exports.appendStats = appendStats;
9
+ exports.readStatsForWindow = readStatsForWindow;
10
+ exports.aggregateStats = aggregateStats;
11
+ exports.rotateStats = rotateStats;
12
+ const fs_1 = require("fs");
13
+ const path_1 = __importDefault(require("path"));
14
+ function resolveStatsDir(paths) {
15
+ return path_1.default.join(paths.baseDir, "stats");
16
+ }
17
+ function formatDate(date) {
18
+ return date.toISOString().split("T")[0];
19
+ }
20
+ function getStatsFilePath(statsDir, date) {
21
+ return path_1.default.join(statsDir, `stats-${formatDate(date)}.jsonl`);
22
+ }
23
+ async function ensureStatsDir(paths) {
24
+ const statsDir = resolveStatsDir(paths);
25
+ await fs_1.promises.mkdir(statsDir, { recursive: true });
26
+ }
27
+ async function appendStats(paths, stats) {
28
+ await ensureStatsDir(paths);
29
+ const statsDir = resolveStatsDir(paths);
30
+ const filePath = getStatsFilePath(statsDir, stats.timestamp);
31
+ const line = `${JSON.stringify(stats)}\n`;
32
+ await fs_1.promises.appendFile(filePath, line, "utf8");
33
+ }
34
+ async function readStatsForWindow(paths, windowDays = 7) {
35
+ await ensureStatsDir(paths);
36
+ const statsDir = resolveStatsDir(paths);
37
+ const stats = [];
38
+ const now = new Date();
39
+ const cutoff = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
40
+ // Read files for the window period
41
+ for (let i = 0; i <= windowDays; i++) {
42
+ const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
43
+ const filePath = getStatsFilePath(statsDir, date);
44
+ try {
45
+ const raw = await fs_1.promises.readFile(filePath, "utf8");
46
+ const lines = raw.split("\n").filter((line) => line.trim().length > 0);
47
+ for (const line of lines) {
48
+ try {
49
+ const entry = JSON.parse(line);
50
+ entry.timestamp = new Date(entry.timestamp);
51
+ if (entry.timestamp >= cutoff) {
52
+ stats.push(entry);
53
+ }
54
+ }
55
+ catch {
56
+ // Skip malformed lines
57
+ }
58
+ }
59
+ }
60
+ catch (error) {
61
+ if (error.code !== "ENOENT") {
62
+ throw error;
63
+ }
64
+ }
65
+ }
66
+ return stats.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
67
+ }
68
+ async function aggregateStats(paths, windowMs = 7 * 24 * 60 * 60 * 1000) {
69
+ const windowDays = Math.ceil(windowMs / (24 * 60 * 60 * 1000));
70
+ const stats = await readStatsForWindow(paths, windowDays);
71
+ const cutoff = Date.now() - windowMs;
72
+ const filtered = stats.filter((s) => s.timestamp.getTime() >= cutoff);
73
+ if (filtered.length === 0) {
74
+ return {
75
+ window: formatWindowString(windowMs),
76
+ total: 0,
77
+ success: 0,
78
+ errors: 0,
79
+ avgLatencyMs: null,
80
+ p50LatencyMs: null,
81
+ p95LatencyMs: null,
82
+ p99LatencyMs: null,
83
+ totalTokens: 0,
84
+ tokensPerHour: null,
85
+ byModel: {},
86
+ byEndpoint: {}
87
+ };
88
+ }
89
+ const latencies = filtered.map((s) => s.latencyMs).sort((a, b) => a - b);
90
+ const successCount = filtered.filter((s) => !s.errorType && s.statusCode >= 200 && s.statusCode < 400).length;
91
+ const errorCount = filtered.filter((s) => s.errorType || s.statusCode >= 400).length;
92
+ let totalTokens = 0;
93
+ const byModel = {};
94
+ const byEndpoint = {};
95
+ for (const stat of filtered) {
96
+ const tokens = stat.totalTokens ?? 0;
97
+ totalTokens += tokens;
98
+ // Aggregate by model
99
+ if (stat.publicModel) {
100
+ if (!byModel[stat.publicModel]) {
101
+ byModel[stat.publicModel] = { count: 0, sumLatency: 0, tokens: 0 };
102
+ }
103
+ byModel[stat.publicModel].count += 1;
104
+ byModel[stat.publicModel].sumLatency += stat.latencyMs;
105
+ byModel[stat.publicModel].tokens += tokens;
106
+ }
107
+ // Aggregate by endpoint
108
+ if (stat.endpointId) {
109
+ if (!byEndpoint[stat.endpointId]) {
110
+ byEndpoint[stat.endpointId] = { count: 0, sumLatency: 0, tokens: 0, errors: 0, name: stat.endpointName ?? "unknown" };
111
+ }
112
+ byEndpoint[stat.endpointId].count += 1;
113
+ byEndpoint[stat.endpointId].sumLatency += stat.latencyMs;
114
+ byEndpoint[stat.endpointId].tokens += tokens;
115
+ if (stat.errorType || stat.statusCode >= 400) {
116
+ byEndpoint[stat.endpointId].errors += 1;
117
+ }
118
+ }
119
+ }
120
+ // Calculate percentiles
121
+ const p50 = percentile(latencies, 50);
122
+ const p95 = percentile(latencies, 95);
123
+ const p99 = percentile(latencies, 99);
124
+ const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
125
+ // Calculate tokens per hour
126
+ const windowHours = windowMs / (60 * 60 * 1000);
127
+ const tokensPerHour = windowHours > 0 ? totalTokens / windowHours : null;
128
+ // Transform aggregations to final format
129
+ const byModelFinal = {};
130
+ for (const [model, data] of Object.entries(byModel)) {
131
+ byModelFinal[model] = {
132
+ count: data.count,
133
+ avgLatencyMs: Math.round(data.sumLatency / data.count),
134
+ tokens: data.tokens
135
+ };
136
+ }
137
+ const byEndpointFinal = {};
138
+ for (const [id, data] of Object.entries(byEndpoint)) {
139
+ byEndpointFinal[id] = {
140
+ count: data.count,
141
+ avgLatencyMs: Math.round(data.sumLatency / data.count),
142
+ tokens: data.tokens,
143
+ errors: data.errors
144
+ };
145
+ }
146
+ return {
147
+ window: formatWindowString(windowMs),
148
+ total: filtered.length,
149
+ success: successCount,
150
+ errors: errorCount,
151
+ avgLatencyMs: Math.round(avgLatency),
152
+ p50LatencyMs: p50,
153
+ p95LatencyMs: p95,
154
+ p99LatencyMs: p99,
155
+ totalTokens,
156
+ tokensPerHour: tokensPerHour !== null ? Math.round(tokensPerHour) : null,
157
+ byModel: byModelFinal,
158
+ byEndpoint: byEndpointFinal
159
+ };
160
+ }
161
+ function percentile(sortedArr, p) {
162
+ if (sortedArr.length === 0)
163
+ return null;
164
+ const index = Math.ceil((p / 100) * sortedArr.length) - 1;
165
+ return Math.round(sortedArr[Math.max(0, index)]);
166
+ }
167
+ function formatWindowString(ms) {
168
+ const hours = ms / (60 * 60 * 1000);
169
+ if (hours < 24)
170
+ return `${Math.round(hours)}h`;
171
+ return `${Math.round(hours / 24)}d`;
172
+ }
173
+ /**
174
+ * Rotate stats files - delete files older than retentionDays
175
+ */
176
+ async function rotateStats(paths, retentionDays = 30) {
177
+ await ensureStatsDir(paths);
178
+ const statsDir = resolveStatsDir(paths);
179
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
180
+ let deleted = 0;
181
+ try {
182
+ const files = await fs_1.promises.readdir(statsDir);
183
+ for (const file of files) {
184
+ if (!file.startsWith("stats-") || !file.endsWith(".jsonl")) {
185
+ continue;
186
+ }
187
+ // Extract date from filename: stats-YYYY-MM-DD.jsonl
188
+ const match = file.match(/^stats-(\d{4}-\d{2}-\d{2})\.jsonl$/);
189
+ if (!match)
190
+ continue;
191
+ const fileDate = new Date(match[1]);
192
+ if (fileDate < cutoff) {
193
+ await fs_1.promises.unlink(path_1.default.join(statsDir, file));
194
+ deleted += 1;
195
+ }
196
+ }
197
+ }
198
+ catch (error) {
199
+ if (error.code !== "ENOENT") {
200
+ throw error;
201
+ }
202
+ }
203
+ return deleted;
204
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.proxyUpstream = proxyUpstream;
4
+ exports.classifyUpstreamError = classifyUpstreamError;
5
+ exports.classifyHttpStatus = classifyHttpStatus;
6
+ const undici_1 = require("undici");
7
+ const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
8
+ const TLS_VERIFY_ERROR_CODES = new Set([
9
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
10
+ "SELF_SIGNED_CERT_IN_CHAIN",
11
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
12
+ "CERT_HAS_EXPIRED",
13
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
14
+ "ERR_TLS_CERT_ALTNAME_INVALID",
15
+ "ERR_TLS_CERT_SIGNATURE_ALGORITHM_UNSUPPORTED",
16
+ ]);
17
+ async function proxyUpstream(endpoint, path, payload, headers, timeoutMs, signal, options) {
18
+ const url = new URL(path, endpoint.baseUrl).toString();
19
+ const dispatcher = endpoint.insecureTls
20
+ ? new undici_1.Agent({ connect: { rejectUnauthorized: false } })
21
+ : undefined;
22
+ const requestHeaders = {
23
+ "content-type": "application/json",
24
+ accept: "application/json",
25
+ ...filterHeaders(headers)
26
+ };
27
+ if (endpoint.apiKey && !options?.skipDefaultAuth && !requestHeaders.authorization) {
28
+ requestHeaders.authorization = `Bearer ${endpoint.apiKey}`;
29
+ }
30
+ const response = await (0, undici_1.request)(url, {
31
+ method: "POST",
32
+ body: JSON.stringify(payload),
33
+ headers: requestHeaders,
34
+ dispatcher,
35
+ signal,
36
+ headersTimeout: timeoutMs,
37
+ bodyTimeout: timeoutMs
38
+ });
39
+ return {
40
+ statusCode: response.statusCode,
41
+ headers: response.headers,
42
+ body: response.body
43
+ };
44
+ }
45
+ function classifyUpstreamError(error) {
46
+ if (error instanceof Error) {
47
+ const err = error;
48
+ if (typeof err.type === "string" && typeof err.retryable === "boolean") {
49
+ return err;
50
+ }
51
+ const code = err.code;
52
+ if (isTlsVerifyError(err, code)) {
53
+ err.type = "tls_verify_failed";
54
+ err.retryable = true;
55
+ return err;
56
+ }
57
+ // Connection errors
58
+ if (code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND") {
59
+ err.type = "connection";
60
+ err.retryable = true;
61
+ return err;
62
+ }
63
+ // Undici timeout errors
64
+ if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
65
+ err.type = "timeout";
66
+ err.retryable = true;
67
+ return err;
68
+ }
69
+ if (err.name === "AbortError") {
70
+ err.type = "timeout";
71
+ err.retryable = true;
72
+ return err;
73
+ }
74
+ // Socket/stream errors during transfer
75
+ if (code === "ERR_STREAM_PREMATURE_CLOSE" || code === "EPIPE" || code === "ECONNABORTED") {
76
+ err.type = "stream_error";
77
+ err.retryable = true;
78
+ return err;
79
+ }
80
+ err.type = "unknown";
81
+ err.retryable = false;
82
+ return err;
83
+ }
84
+ const fallback = new Error("Unknown upstream error");
85
+ fallback.type = "unknown";
86
+ fallback.retryable = false;
87
+ return fallback;
88
+ }
89
+ function isTlsVerifyError(error, code) {
90
+ if (code && TLS_VERIFY_ERROR_CODES.has(code)) {
91
+ return true;
92
+ }
93
+ const message = error.message.toLowerCase();
94
+ return (message.includes("unable to verify the first certificate") ||
95
+ message.includes("self-signed certificate") ||
96
+ message.includes("certificate verify failed"));
97
+ }
98
+ function classifyHttpStatus(statusCode) {
99
+ if (statusCode === 429) {
100
+ return { retryable: true, type: "rate_limited" };
101
+ }
102
+ if (RETRYABLE_STATUSES.has(statusCode)) {
103
+ return { retryable: true, type: "upstream_5xx" };
104
+ }
105
+ if ([400, 401, 403].includes(statusCode)) {
106
+ return { retryable: false, type: "upstream_4xx" };
107
+ }
108
+ if (statusCode >= 400) {
109
+ return { retryable: false, type: "upstream_4xx" };
110
+ }
111
+ return { retryable: false, type: "ok" };
112
+ }
113
+ function filterHeaders(headers) {
114
+ const filtered = {};
115
+ for (const [key, value] of Object.entries(headers)) {
116
+ if (!value) {
117
+ continue;
118
+ }
119
+ const lower = key.toLowerCase();
120
+ if (lower === "host" || lower === "content-length") {
121
+ continue;
122
+ }
123
+ filtered[lower] = Array.isArray(value) ? value.join(", ") : value;
124
+ }
125
+ return filtered;
126
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });