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,450 @@
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.registerSessionRoutes = registerSessionRoutes;
7
+ const sessionRepository_1 = require("../storage/sessionRepository");
8
+ const imageCache_1 = require("../storage/imageCache");
9
+ const files_1 = require("../storage/files");
10
+ const fs_1 = require("fs");
11
+ const path_1 = __importDefault(require("path"));
12
+ const router_1 = require("../routing/router");
13
+ const repositories_1 = require("../storage/repositories");
14
+ /**
15
+ * Sessions Routes
16
+ *
17
+ * REST API for managing chat sessions in the playground.
18
+ *
19
+ * Endpoints:
20
+ * GET /admin/sessions - List all sessions
21
+ * POST /admin/sessions - Create a new session
22
+ * GET /admin/sessions/:id - Get session by ID
23
+ * PUT /admin/sessions/:id - Update session metadata
24
+ * DELETE /admin/sessions/:id - Delete a session
25
+ * POST /admin/sessions/:id/messages - Add a message to session
26
+ * PATCH /admin/sessions/:id/messages/:msgIndex - Append to message (streaming)
27
+ *
28
+ * GET /admin/images/:hash - Get cached image by hash
29
+ * POST /admin/images - Store image in cache
30
+ * GET /admin/images/stats - Get image cache stats
31
+ * DELETE /admin/images - Clear image cache
32
+ */
33
+ async function registerSessionRoutes(app) {
34
+ const paths = (0, files_1.resolveStoragePaths)();
35
+ await (0, imageCache_1.ensureMediaCacheReady)(paths);
36
+ app.log.info({ mediaRoot: path_1.default.join(paths.baseDir, "media") }, "Media cache initialized");
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+ // Session CRUD
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+ app.get("/admin/sessions", async (_req, reply) => {
41
+ try {
42
+ const sessions = await (0, sessionRepository_1.listSessions)(paths);
43
+ return reply.send({
44
+ object: "list",
45
+ data: sessions.map((s) => ({
46
+ id: s.id,
47
+ name: s.name,
48
+ model: s.model,
49
+ storageVersion: s.storageVersion,
50
+ titleStatus: s.titleStatus,
51
+ titleUpdatedAt: s.titleUpdatedAt,
52
+ messageCount: s.messages.length,
53
+ createdAt: s.createdAt,
54
+ updatedAt: s.updatedAt,
55
+ })),
56
+ });
57
+ }
58
+ catch (error) {
59
+ app.log.error(error, "Failed to list sessions");
60
+ return reply.status(500).send({
61
+ error: { message: "Failed to list sessions", type: "internal_error" },
62
+ });
63
+ }
64
+ });
65
+ app.post("/admin/sessions", async (req, reply) => {
66
+ try {
67
+ const { name, model } = req.body || {};
68
+ const session = await (0, sessionRepository_1.createSession)(paths, { name, model });
69
+ return reply.status(201).send(toApiSession(session));
70
+ }
71
+ catch (error) {
72
+ app.log.error(error, "Failed to create session");
73
+ return reply.status(500).send({
74
+ error: { message: "Failed to create session", type: "internal_error" },
75
+ });
76
+ }
77
+ });
78
+ app.post("/admin/sessions/:id/auto-title", async (req, reply) => {
79
+ try {
80
+ const session = await (0, sessionRepository_1.getSession)(paths, req.params.id);
81
+ if (!session) {
82
+ return reply.status(404).send({
83
+ error: { message: "Session not found", type: "not_found" },
84
+ });
85
+ }
86
+ const seedText = req.body?.seedText?.trim() || extractSeedTextFromSession(session.messages);
87
+ if (!seedText) {
88
+ return reply.status(400).send({
89
+ error: { message: "seedText is required for auto-title", type: "invalid_request" },
90
+ });
91
+ }
92
+ const model = req.body?.model ||
93
+ session.model ||
94
+ (await (0, repositories_1.pickBestModelByCapabilities)(paths, { requiredInput: ["text"], requiredOutput: ["text"] }, "llm"));
95
+ let generated = false;
96
+ let title = fallbackTitleFromSeed(seedText);
97
+ if (model) {
98
+ try {
99
+ title = await generateTitleFromModel(paths, model, seedText);
100
+ generated = true;
101
+ }
102
+ catch (error) {
103
+ app.log.warn(error, "Auto-title generation failed, using fallback title");
104
+ }
105
+ }
106
+ const updated = await (0, sessionRepository_1.updateSession)(paths, req.params.id, {
107
+ name: title,
108
+ titleStatus: generated ? "generated" : "failed",
109
+ titleUpdatedAt: new Date(),
110
+ });
111
+ if (!updated) {
112
+ return reply.status(404).send({
113
+ error: { message: "Session not found", type: "not_found" },
114
+ });
115
+ }
116
+ return reply.send({
117
+ id: updated.id,
118
+ name: updated.name,
119
+ titleStatus: updated.titleStatus,
120
+ titleUpdatedAt: updated.titleUpdatedAt,
121
+ generated,
122
+ model,
123
+ });
124
+ }
125
+ catch (error) {
126
+ app.log.error(error, "Failed to auto-title session");
127
+ return reply.status(500).send({
128
+ error: { message: "Failed to auto-title session", type: "internal_error" },
129
+ });
130
+ }
131
+ });
132
+ app.get("/admin/sessions/:id", async (req, reply) => {
133
+ try {
134
+ const session = await (0, sessionRepository_1.getSession)(paths, req.params.id);
135
+ if (!session) {
136
+ return reply.status(404).send({
137
+ error: { message: "Session not found", type: "not_found" },
138
+ });
139
+ }
140
+ return reply.send(toApiSession(session));
141
+ }
142
+ catch (error) {
143
+ app.log.error(error, "Failed to get session");
144
+ return reply.status(500).send({
145
+ error: { message: "Failed to get session", type: "internal_error" },
146
+ });
147
+ }
148
+ });
149
+ app.put("/admin/sessions/:id", async (req, reply) => {
150
+ try {
151
+ const updates = {};
152
+ if (req.body?.name !== undefined)
153
+ updates.name = req.body.name;
154
+ if (req.body?.model !== undefined)
155
+ updates.model = req.body.model;
156
+ const session = await (0, sessionRepository_1.updateSession)(paths, req.params.id, updates);
157
+ if (!session) {
158
+ return reply.status(404).send({
159
+ error: { message: "Session not found", type: "not_found" },
160
+ });
161
+ }
162
+ return reply.send(toApiSession(session));
163
+ }
164
+ catch (error) {
165
+ app.log.error(error, "Failed to update session");
166
+ return reply.status(500).send({
167
+ error: { message: "Failed to update session", type: "internal_error" },
168
+ });
169
+ }
170
+ });
171
+ app.delete("/admin/sessions/:id", async (req, reply) => {
172
+ try {
173
+ const deleted = await (0, sessionRepository_1.deleteSession)(paths, req.params.id);
174
+ if (!deleted) {
175
+ return reply.status(404).send({
176
+ error: { message: "Session not found", type: "not_found" },
177
+ });
178
+ }
179
+ return reply.status(204).send();
180
+ }
181
+ catch (error) {
182
+ app.log.error(error, "Failed to delete session");
183
+ return reply.status(500).send({
184
+ error: { message: "Failed to delete session", type: "internal_error" },
185
+ });
186
+ }
187
+ });
188
+ // ─────────────────────────────────────────────────────────────────────────────
189
+ // Message management
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+ app.post("/admin/sessions/:id/messages", async (req, reply) => {
192
+ try {
193
+ const message = await (0, sessionRepository_1.addMessage)(paths, req.params.id, normalizeIncomingMessage(req.body));
194
+ if (!message) {
195
+ return reply.status(404).send({
196
+ error: { message: "Session not found", type: "not_found" },
197
+ });
198
+ }
199
+ return reply.status(201).send({
200
+ messageId: message.id,
201
+ createdAt: message.createdAt,
202
+ });
203
+ }
204
+ catch (error) {
205
+ app.log.error(error, "Failed to add message");
206
+ return reply.status(500).send({
207
+ error: { message: "Failed to add message", type: "internal_error" },
208
+ });
209
+ }
210
+ });
211
+ app.patch("/admin/sessions/:id/messages/:messageId", async (req, reply) => {
212
+ try {
213
+ const success = await (0, sessionRepository_1.appendMessageContent)(paths, req.params.id, req.params.messageId, req.body?.content || "");
214
+ if (!success) {
215
+ return reply.status(404).send({
216
+ error: { message: "Session or message not found", type: "not_found" },
217
+ });
218
+ }
219
+ return reply.send({ success: true });
220
+ }
221
+ catch (error) {
222
+ app.log.error(error, "Failed to append to message");
223
+ return reply.status(500).send({
224
+ error: { message: "Failed to append to message", type: "internal_error" },
225
+ });
226
+ }
227
+ });
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+ // Media cache (/admin/media) with image alias compatibility (/admin/images)
230
+ // ─────────────────────────────────────────────────────────────────────────────
231
+ registerMediaCacheRoutes("/admin/media");
232
+ registerMediaCacheRoutes("/admin/images");
233
+ function registerMediaCacheRoutes(prefix) {
234
+ app.get(`${prefix}/stats`, async (_req, reply) => {
235
+ try {
236
+ const stats = await (0, imageCache_1.getCacheStats)(paths);
237
+ return reply.send(stats);
238
+ }
239
+ catch (error) {
240
+ app.log.error(error, "Failed to get media cache stats");
241
+ return reply.status(500).send({
242
+ error: { message: "Failed to get cache stats", type: "internal_error" },
243
+ });
244
+ }
245
+ });
246
+ app.get(`${prefix}/:hash`, async (req, reply) => {
247
+ try {
248
+ const filePath = await (0, imageCache_1.getMediaPath)(paths, req.params.hash);
249
+ const entry = await (0, imageCache_1.getMediaEntry)(paths, req.params.hash);
250
+ if (!filePath || !entry) {
251
+ return reply.status(404).send({
252
+ error: { message: "Media not found", type: "not_found" },
253
+ });
254
+ }
255
+ const buffer = await fs_1.promises.readFile(filePath);
256
+ return reply
257
+ .header("Content-Type", entry.mimeType || guessMimeType(filePath))
258
+ .header("Cache-Control", "public, max-age=31536000, immutable")
259
+ .send(buffer);
260
+ }
261
+ catch (error) {
262
+ app.log.error(error, "Failed to get media");
263
+ return reply.status(500).send({
264
+ error: { message: "Failed to get media", type: "internal_error" },
265
+ });
266
+ }
267
+ });
268
+ app.post(`${prefix}`, async (req, reply) => {
269
+ try {
270
+ if (!req.body?.data) {
271
+ return reply.status(400).send({
272
+ error: { message: "Missing media data", type: "invalid_request" },
273
+ });
274
+ }
275
+ const result = await (0, imageCache_1.storeMedia)(paths, req.body.data, {
276
+ model: req.body.model,
277
+ mimeType: req.body.mimeType,
278
+ });
279
+ return reply.status(201).send({
280
+ hash: result.hash,
281
+ mimeType: result.mimeType,
282
+ url: `${prefix}/${result.hash}`,
283
+ evicted: result.evicted,
284
+ });
285
+ }
286
+ catch (error) {
287
+ app.log.error(error, "Failed to store media");
288
+ return reply.status(500).send({
289
+ error: { message: "Failed to store media", type: "internal_error" },
290
+ });
291
+ }
292
+ });
293
+ app.delete(`${prefix}`, async (_req, reply) => {
294
+ try {
295
+ const deleted = await (0, imageCache_1.clearCache)(paths);
296
+ return reply.send({ deleted });
297
+ }
298
+ catch (error) {
299
+ app.log.error(error, "Failed to clear media cache");
300
+ return reply.status(500).send({
301
+ error: { message: "Failed to clear cache", type: "internal_error" },
302
+ });
303
+ }
304
+ });
305
+ }
306
+ }
307
+ function normalizeIncomingMessage(body) {
308
+ return {
309
+ role: body.role ?? "user",
310
+ content: body.content ?? "",
311
+ name: body.name,
312
+ tool_calls: body.tool_calls,
313
+ tool_call_id: body.tool_call_id,
314
+ images: body.images,
315
+ model: body.model,
316
+ };
317
+ }
318
+ function toApiSession(session) {
319
+ return {
320
+ ...session,
321
+ messages: session.messages.map((message) => ({
322
+ ...message,
323
+ timestamp: message.createdAt,
324
+ })),
325
+ };
326
+ }
327
+ function guessMimeType(filePath) {
328
+ const ext = path_1.default.extname(filePath).slice(1).toLowerCase();
329
+ const mimeTypes = {
330
+ png: "image/png",
331
+ jpg: "image/jpeg",
332
+ jpeg: "image/jpeg",
333
+ gif: "image/gif",
334
+ webp: "image/webp",
335
+ wav: "audio/wav",
336
+ mp3: "audio/mpeg",
337
+ ogg: "audio/ogg",
338
+ webm: "audio/webm",
339
+ m4a: "audio/mp4",
340
+ };
341
+ return mimeTypes[ext] ?? "application/octet-stream";
342
+ }
343
+ function extractSeedTextFromSession(messages) {
344
+ const firstUserMessage = messages.find((message) => message.role === "user");
345
+ if (!firstUserMessage) {
346
+ return "";
347
+ }
348
+ return textFromContent(firstUserMessage.content);
349
+ }
350
+ function textFromContent(content) {
351
+ if (typeof content === "string") {
352
+ return content.trim();
353
+ }
354
+ if (!Array.isArray(content)) {
355
+ return "";
356
+ }
357
+ const parts = [];
358
+ for (const part of content) {
359
+ if (part.type === "text") {
360
+ parts.push(part.text);
361
+ }
362
+ }
363
+ return parts.join(" ").trim();
364
+ }
365
+ function fallbackTitleFromSeed(seed) {
366
+ const cleaned = sanitizeTitle(seed);
367
+ if (!cleaned) {
368
+ return "New Session";
369
+ }
370
+ const words = cleaned.split(/\s+/).slice(0, 7).join(" ");
371
+ return words.length > 60 ? words.slice(0, 60).trim() : words;
372
+ }
373
+ async function generateTitleFromModel(paths, model, seedText) {
374
+ const prompt = [
375
+ "Generate a short title for this chat.",
376
+ "Rules: 3-7 words, sentence case, no quotes, no punctuation at end.",
377
+ "Return only the title text.",
378
+ `Chat seed: ${seedText}`,
379
+ ].join("\n");
380
+ const payload = {
381
+ model,
382
+ stream: false,
383
+ temperature: 0,
384
+ max_tokens: 32,
385
+ messages: [
386
+ {
387
+ role: "system",
388
+ content: "You write concise conversation titles.",
389
+ },
390
+ {
391
+ role: "user",
392
+ content: prompt,
393
+ },
394
+ ],
395
+ };
396
+ const outcome = await (0, router_1.routeRequest)(paths, model, "/v1/chat/completions", payload, {}, AbortSignal.timeout(15_000), {
397
+ requiredInput: ["text"],
398
+ requiredOutput: ["text"],
399
+ });
400
+ const payloadJson = await readJsonBody(outcome.attempt.response.body);
401
+ const text = extractAssistantText(payloadJson);
402
+ const sanitized = sanitizeTitle(text);
403
+ if (!sanitized) {
404
+ throw new Error("Model did not return a valid title");
405
+ }
406
+ return sanitized;
407
+ }
408
+ async function readJsonBody(stream) {
409
+ const chunks = [];
410
+ for await (const chunk of stream) {
411
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
412
+ }
413
+ const text = Buffer.concat(chunks).toString("utf8");
414
+ return JSON.parse(text);
415
+ }
416
+ function extractAssistantText(payload) {
417
+ if (!payload || typeof payload !== "object") {
418
+ return "";
419
+ }
420
+ const choices = payload.choices;
421
+ if (!Array.isArray(choices) || choices.length === 0) {
422
+ return "";
423
+ }
424
+ const content = choices[0]?.message?.content;
425
+ if (typeof content === "string") {
426
+ return content;
427
+ }
428
+ if (!Array.isArray(content)) {
429
+ return "";
430
+ }
431
+ const parts = [];
432
+ for (const part of content) {
433
+ if (part && typeof part === "object" && typeof part.text === "string") {
434
+ parts.push(part.text);
435
+ }
436
+ }
437
+ return parts.join(" ");
438
+ }
439
+ function sanitizeTitle(input) {
440
+ const normalized = input
441
+ .replace(/[\r\n]+/g, " ")
442
+ .replace(/["'`]+/g, "")
443
+ .replace(/\s+/g, " ")
444
+ .trim();
445
+ const trimmed = normalized.replace(/[.,;:!?]+$/g, "").trim();
446
+ if (!trimmed) {
447
+ return "";
448
+ }
449
+ return trimmed.length > 60 ? trimmed.slice(0, 60).trim() : trimmed;
450
+ }