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,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
|
+
}
|