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.
- package/.github/instructions/ui.instructions.md +42 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +71 -0
- package/.github/workflows/release.yml +48 -0
- package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
- package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
- package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
- package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
- package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
- package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +131 -0
- package/README.md +552 -0
- package/assets/agent-mode.png +0 -0
- package/assets/categorize.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/endpoint-proxy.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/mcp-generate-image.png +0 -0
- package/assets/mcp-understand-image.png +0 -0
- package/assets/peek-token-flow.png +0 -0
- package/assets/playground.png +0 -0
- package/assets/sankey.png +0 -0
- package/cli/index.ts +2805 -0
- package/cli/legacyRewrite.ts +108 -0
- package/cli/modelRef.ts +24 -0
- package/dist/cli/index.js +2536 -0
- package/dist/cli/legacyRewrite.js +92 -0
- package/dist/cli/modelRef.js +20 -0
- package/dist/src/benchmark/artifacts.js +131 -0
- package/dist/src/benchmark/capabilityClassifier.js +81 -0
- package/dist/src/benchmark/capabilityStore.js +144 -0
- package/dist/src/benchmark/config.js +238 -0
- package/dist/src/benchmark/gates.js +118 -0
- package/dist/src/benchmark/jobs.js +252 -0
- package/dist/src/benchmark/runner.js +1847 -0
- package/dist/src/benchmark/schema.js +353 -0
- package/dist/src/benchmark/suites.js +314 -0
- package/dist/src/benchmark/tinyQaDataset.js +422 -0
- package/dist/src/benchmark/types.js +25 -0
- package/dist/src/config.js +47 -0
- package/dist/src/index.js +178 -0
- package/dist/src/mcp/client.js +215 -0
- package/dist/src/mcp/discovery.js +226 -0
- package/dist/src/mcp/policy.js +65 -0
- package/dist/src/mcp/registry.js +129 -0
- package/dist/src/mcp/service.js +460 -0
- package/dist/src/middleware/auth.js +179 -0
- package/dist/src/middleware/requestCapture.js +192 -0
- package/dist/src/middleware/requestStats.js +118 -0
- package/dist/src/pools/builder.js +132 -0
- package/dist/src/pools/repository.js +69 -0
- package/dist/src/pools/scheduler.js +360 -0
- package/dist/src/pools/types.js +2 -0
- package/dist/src/protocols/adapters/dashscope.js +267 -0
- package/dist/src/protocols/adapters/inferenceV2.js +346 -0
- package/dist/src/protocols/adapters/openai.js +27 -0
- package/dist/src/protocols/registry.js +99 -0
- package/dist/src/protocols/types.js +2 -0
- package/dist/src/providers/health.js +153 -0
- package/dist/src/providers/importer.js +289 -0
- package/dist/src/providers/modelRegistry.js +313 -0
- package/dist/src/providers/repository.js +361 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/routes/admin.js +531 -0
- package/dist/src/routes/audio.js +295 -0
- package/dist/src/routes/chat.js +240 -0
- package/dist/src/routes/embeddings.js +157 -0
- package/dist/src/routes/images.js +288 -0
- package/dist/src/routes/mcp.js +256 -0
- package/dist/src/routes/mcpService.js +100 -0
- package/dist/src/routes/models.js +48 -0
- package/dist/src/routes/responses.js +711 -0
- package/dist/src/routes/sessions.js +450 -0
- package/dist/src/routes/stats.js +270 -0
- package/dist/src/routes/ui.js +97 -0
- package/dist/src/routes/videos.js +107 -0
- package/dist/src/routing/router.js +338 -0
- package/dist/src/services/imageGeneration.js +280 -0
- package/dist/src/services/imageUnderstanding.js +352 -0
- package/dist/src/services/videoGeneration.js +79 -0
- package/dist/src/storage/captureRepository.js +1591 -0
- package/dist/src/storage/files.js +157 -0
- package/dist/src/storage/imageCache.js +346 -0
- package/dist/src/storage/repositories.js +388 -0
- package/dist/src/storage/sessionRepository.js +370 -0
- package/dist/src/storage/statsRepository.js +204 -0
- package/dist/src/transport/httpClient.js +126 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/messageMedia.js +285 -0
- package/dist/src/utils/modelCapabilities.js +108 -0
- package/dist/src/utils/modelDiscovery.js +170 -0
- package/dist/src/version.js +5 -0
- package/dist/src/workers/captureRetention.js +25 -0
- package/dist/src/workers/configWatcher.js +91 -0
- package/dist/src/workers/healthChecker.js +21 -0
- package/dist/src/workers/statsRotation.js +41 -0
- package/docs/LLM/output_schema.md +312 -0
- package/docs/benchmark.md +208 -0
- package/docs/mcp-guidelines.md +125 -0
- package/docs/mcp-service.md +178 -0
- package/docs/opencode.md +86 -0
- package/docs/providers.md +79 -0
- package/examples/benchmark.config.yaml +28 -0
- package/examples/providers/alibaba-dashscope.yaml +88 -0
- package/examples/providers/alibaba-llm.yaml +64 -0
- package/examples/providers/alibaba-registry.yaml +7 -0
- package/examples/providers/inference-v2-ray.yaml +29 -0
- package/examples/scenarios/assets/omni-call-sample.wav +0 -0
- package/examples/scenarios/custom.jsonl +5 -0
- package/examples/scenarios/custom.yaml +40 -0
- package/model-form-v2.png +0 -0
- package/package.json +66 -0
- package/provider-form-v2.png +0 -0
- package/provider-form.png +0 -0
- package/scripts/manual-test.sh +11 -0
- package/scripts/version-from-git.js +23 -0
- package/src/benchmark/artifacts.ts +149 -0
- package/src/benchmark/capabilityClassifier.ts +99 -0
- package/src/benchmark/capabilityStore.ts +174 -0
- package/src/benchmark/config.ts +337 -0
- package/src/benchmark/gates.ts +164 -0
- package/src/benchmark/jobs.ts +312 -0
- package/src/benchmark/runner.ts +2519 -0
- package/src/benchmark/schema.ts +443 -0
- package/src/benchmark/suites.ts +323 -0
- package/src/benchmark/tinyQaDataset.ts +428 -0
- package/src/benchmark/types.ts +442 -0
- package/src/config.ts +44 -0
- package/src/index.ts +195 -0
- package/src/mcp/client.ts +305 -0
- package/src/mcp/discovery.ts +266 -0
- package/src/mcp/policy.ts +105 -0
- package/src/mcp/registry.ts +164 -0
- package/src/mcp/service.ts +611 -0
- package/src/middleware/auth.ts +251 -0
- package/src/middleware/requestCapture.ts +245 -0
- package/src/middleware/requestStats.ts +163 -0
- package/src/pools/builder.ts +159 -0
- package/src/pools/repository.ts +71 -0
- package/src/pools/scheduler.ts +425 -0
- package/src/pools/types.ts +117 -0
- package/src/protocols/adapters/dashscope.ts +335 -0
- package/src/protocols/adapters/inferenceV2.ts +428 -0
- package/src/protocols/adapters/openai.ts +32 -0
- package/src/protocols/registry.ts +117 -0
- package/src/protocols/types.ts +81 -0
- package/src/providers/health.ts +207 -0
- package/src/providers/importer.ts +402 -0
- package/src/providers/modelRegistry.ts +415 -0
- package/src/providers/repository.ts +439 -0
- package/src/providers/types.ts +113 -0
- package/src/routes/admin.ts +666 -0
- package/src/routes/audio.ts +372 -0
- package/src/routes/chat.ts +301 -0
- package/src/routes/embeddings.ts +197 -0
- package/src/routes/images.ts +356 -0
- package/src/routes/mcp.ts +320 -0
- package/src/routes/mcpService.ts +114 -0
- package/src/routes/models.ts +50 -0
- package/src/routes/responses.ts +872 -0
- package/src/routes/sessions.ts +558 -0
- package/src/routes/stats.ts +312 -0
- package/src/routes/ui.ts +96 -0
- package/src/routes/videos.ts +132 -0
- package/src/routing/router.ts +501 -0
- package/src/services/imageGeneration.ts +396 -0
- package/src/services/imageUnderstanding.ts +449 -0
- package/src/services/videoGeneration.ts +127 -0
- package/src/storage/captureRepository.ts +1835 -0
- package/src/storage/files.ts +178 -0
- package/src/storage/imageCache.ts +405 -0
- package/src/storage/repositories.ts +494 -0
- package/src/storage/sessionRepository.ts +419 -0
- package/src/storage/statsRepository.ts +238 -0
- package/src/transport/httpClient.ts +145 -0
- package/src/types.ts +322 -0
- package/src/utils/messageMedia.ts +293 -0
- package/src/utils/modelCapabilities.ts +161 -0
- package/src/utils/modelDiscovery.ts +203 -0
- package/src/workers/captureRetention.ts +25 -0
- package/src/workers/configWatcher.ts +115 -0
- package/src/workers/healthChecker.ts +22 -0
- package/src/workers/statsRotation.ts +49 -0
- package/tests/benchmarkAdminRoutes.test.ts +82 -0
- package/tests/benchmarkBasics.test.ts +116 -0
- package/tests/captureAdminRoutes.test.ts +420 -0
- package/tests/captureRepository.test.ts +797 -0
- package/tests/cliLegacyRewrite.test.ts +45 -0
- package/tests/imageGeneration.service.test.ts +107 -0
- package/tests/imageUnderstanding.service.test.ts +123 -0
- package/tests/mcpPolicy.test.ts +105 -0
- package/tests/mcpService.test.ts +1245 -0
- package/tests/modelRef.test.ts +23 -0
- package/tests/modelsRoutes.test.ts +154 -0
- package/tests/sessionMediaCache.test.ts +167 -0
- package/tests/statsRoutes.test.ts +323 -0
- package/tsconfig.json +15 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +8521 -0
- package/ui/package.json +52 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/assets/apple-touch-icon.png +0 -0
- package/ui/public/assets/favicon-16.png +0 -0
- package/ui/public/assets/favicon-32.png +0 -0
- package/ui/public/assets/icon-192.png +0 -0
- package/ui/public/assets/icon-512.png +0 -0
- package/ui/src/App.tsx +27 -0
- package/ui/src/api/client.ts +1503 -0
- package/ui/src/components/EndpointUsageGuide.tsx +361 -0
- package/ui/src/components/Layout.tsx +124 -0
- package/ui/src/components/MessageContent.tsx +365 -0
- package/ui/src/components/ToolCallMessage.tsx +179 -0
- package/ui/src/components/ToolPicker.tsx +442 -0
- package/ui/src/components/messageContentParser.test.ts +41 -0
- package/ui/src/components/messageContentParser.ts +73 -0
- package/ui/src/components/thinkingPreview.test.ts +27 -0
- package/ui/src/components/thinkingPreview.ts +15 -0
- package/ui/src/components/toMermaidSankey.test.ts +78 -0
- package/ui/src/components/toMermaidSankey.ts +56 -0
- package/ui/src/components/ui/button.tsx +58 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/textarea.tsx +21 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +9 -0
- package/ui/src/pages/AgentPlayground.tsx +2010 -0
- package/ui/src/pages/Benchmark.tsx +988 -0
- package/ui/src/pages/Dashboard.tsx +581 -0
- package/ui/src/pages/Peek.tsx +962 -0
- package/ui/src/pages/Settings.tsx +2013 -0
- package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
- package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
- package/ui/src/pages/agentThinkingContent.test.ts +50 -0
- package/ui/src/pages/agentThinkingContent.ts +57 -0
- package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
- package/ui/src/pages/dashboardTokenUsage.ts +36 -0
- package/ui/src/pages/imageUpload.test.ts +39 -0
- package/ui/src/pages/imageUpload.ts +71 -0
- package/ui/src/pages/peekFilters.test.ts +29 -0
- package/ui/src/pages/peekFilters.ts +13 -0
- package/ui/src/pages/peekMedia.test.ts +58 -0
- package/ui/src/pages/peekMedia.ts +148 -0
- package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
- package/ui/src/pages/sessionAutoTitle.ts +106 -0
- package/ui/src/stores/settings.ts +58 -0
- package/ui/src/styles/globals.css +223 -0
- package/ui/src/vite-env.d.ts +8 -0
- package/ui/tailwind.config.js +106 -0
- package/ui/tsconfig.json +32 -0
- 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
|
+
}
|