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,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRequestCaptureMiddleware = registerRequestCaptureMiddleware;
|
|
4
|
+
exports.setCaptureRouting = setCaptureRouting;
|
|
5
|
+
exports.setCaptureDerivedRequest = setCaptureDerivedRequest;
|
|
6
|
+
exports.setCaptureResponseOverride = setCaptureResponseOverride;
|
|
7
|
+
exports.setCaptureError = setCaptureError;
|
|
8
|
+
exports.startCaptureStreamResponse = startCaptureStreamResponse;
|
|
9
|
+
exports.appendCaptureStreamChunk = appendCaptureStreamChunk;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const captureRepository_1 = require("../storage/captureRepository");
|
|
12
|
+
const captureContexts = new WeakMap();
|
|
13
|
+
function meta(reply) {
|
|
14
|
+
return reply;
|
|
15
|
+
}
|
|
16
|
+
async function registerRequestCaptureMiddleware(app, paths) {
|
|
17
|
+
app.addHook("onRequest", async (req) => {
|
|
18
|
+
if (!req.url.startsWith("/v1/"))
|
|
19
|
+
return;
|
|
20
|
+
let enabled = false;
|
|
21
|
+
try {
|
|
22
|
+
enabled = await (0, captureRepository_1.isCaptureEnabled)(paths);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
enabled = false;
|
|
26
|
+
}
|
|
27
|
+
captureContexts.set(req, {
|
|
28
|
+
id: (0, crypto_1.randomUUID)(),
|
|
29
|
+
startedAt: Date.now(),
|
|
30
|
+
enabled,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
app.addHook("preHandler", async (req) => {
|
|
34
|
+
const context = captureContexts.get(req);
|
|
35
|
+
if (!context?.enabled)
|
|
36
|
+
return;
|
|
37
|
+
context.requestBody = safeClone(req.body);
|
|
38
|
+
});
|
|
39
|
+
app.addHook("onSend", async (req, reply, payload) => {
|
|
40
|
+
const context = captureContexts.get(req);
|
|
41
|
+
if (!context?.enabled)
|
|
42
|
+
return payload;
|
|
43
|
+
if (!meta(reply).captureResponseOverride) {
|
|
44
|
+
context.responseBody = payloadToBody(payload);
|
|
45
|
+
context.responseHeaders = reply.getHeaders();
|
|
46
|
+
}
|
|
47
|
+
return payload;
|
|
48
|
+
});
|
|
49
|
+
app.addHook("onResponse", async (req, reply) => {
|
|
50
|
+
const context = captureContexts.get(req);
|
|
51
|
+
if (!context)
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
if (!context.enabled)
|
|
55
|
+
return;
|
|
56
|
+
const replyMeta = meta(reply);
|
|
57
|
+
context.routing = replyMeta.captureRouting;
|
|
58
|
+
context.derivedRequest = replyMeta.captureDerivedRequest;
|
|
59
|
+
context.error = replyMeta.captureError;
|
|
60
|
+
if (replyMeta.captureResponseOverride) {
|
|
61
|
+
context.responseBody = replyMeta.captureResponseOverride.body;
|
|
62
|
+
context.responseHeaders = replyMeta.captureResponseOverride.headers;
|
|
63
|
+
}
|
|
64
|
+
await (0, captureRepository_1.persistCaptureRecord)(paths, {
|
|
65
|
+
route: req.url,
|
|
66
|
+
method: req.method,
|
|
67
|
+
statusCode: reply.statusCode,
|
|
68
|
+
latencyMs: Date.now() - context.startedAt,
|
|
69
|
+
requestHeaders: req.headers,
|
|
70
|
+
responseHeaders: context.responseHeaders ??
|
|
71
|
+
reply.getHeaders(),
|
|
72
|
+
requestBody: context.requestBody,
|
|
73
|
+
responseBody: context.responseBody,
|
|
74
|
+
derivedRequest: context.derivedRequest,
|
|
75
|
+
routing: context.routing,
|
|
76
|
+
error: context.error,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
app.log.warn({ err: error }, "Failed to persist request capture");
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
captureContexts.delete(req);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function setCaptureRouting(reply, routing) {
|
|
88
|
+
meta(reply).captureRouting = routing;
|
|
89
|
+
const context = captureContexts.get(reply.request);
|
|
90
|
+
if (context?.enabled) {
|
|
91
|
+
context.routing = routing;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function setCaptureDerivedRequest(reply, payload) {
|
|
95
|
+
meta(reply).captureDerivedRequest = payload;
|
|
96
|
+
const context = captureContexts.get(reply.request);
|
|
97
|
+
if (context?.enabled) {
|
|
98
|
+
context.derivedRequest = payload;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function setCaptureResponseOverride(reply, body, headers) {
|
|
102
|
+
meta(reply).captureResponseOverride = { body, headers };
|
|
103
|
+
const context = captureContexts.get(reply.request);
|
|
104
|
+
if (context?.enabled) {
|
|
105
|
+
context.responseBody = body;
|
|
106
|
+
if (headers) {
|
|
107
|
+
context.responseHeaders = headers;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function setCaptureError(reply, error) {
|
|
112
|
+
meta(reply).captureError = error;
|
|
113
|
+
const context = captureContexts.get(reply.request);
|
|
114
|
+
if (context?.enabled) {
|
|
115
|
+
context.error = error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function startCaptureStreamResponse(reply, headers, contentType, note) {
|
|
119
|
+
const body = {
|
|
120
|
+
$type: "stream",
|
|
121
|
+
contentType,
|
|
122
|
+
bytes: 0,
|
|
123
|
+
};
|
|
124
|
+
if (note) {
|
|
125
|
+
body.note = note;
|
|
126
|
+
}
|
|
127
|
+
setCaptureResponseOverride(reply, body, headers);
|
|
128
|
+
}
|
|
129
|
+
function appendCaptureStreamChunk(reply, chunk, options) {
|
|
130
|
+
const context = captureContexts.get(reply.request);
|
|
131
|
+
if (!context?.enabled)
|
|
132
|
+
return;
|
|
133
|
+
const body = ensureCaptureStreamBody(context, options?.contentType);
|
|
134
|
+
body.bytes += chunk.byteLength;
|
|
135
|
+
if (isTextLikeStream(body.contentType)) {
|
|
136
|
+
body.text = (body.text ?? "") + chunk.toString("utf8");
|
|
137
|
+
}
|
|
138
|
+
context.responseBody = body;
|
|
139
|
+
if (options?.headers) {
|
|
140
|
+
context.responseHeaders = options.headers;
|
|
141
|
+
}
|
|
142
|
+
meta(reply).captureResponseOverride = {
|
|
143
|
+
body,
|
|
144
|
+
headers: context.responseHeaders,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function safeClone(value) {
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(JSON.stringify(value));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function payloadToBody(payload) {
|
|
156
|
+
if (payload === null || payload === undefined)
|
|
157
|
+
return payload;
|
|
158
|
+
if (Buffer.isBuffer(payload)) {
|
|
159
|
+
return { $type: "buffer", base64: payload.toString("base64"), bytes: payload.byteLength };
|
|
160
|
+
}
|
|
161
|
+
if (typeof payload === "string") {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(payload);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return payload;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return payload;
|
|
170
|
+
}
|
|
171
|
+
function ensureCaptureStreamBody(context, contentType) {
|
|
172
|
+
const existing = context.responseBody;
|
|
173
|
+
if (existing?.$type === "stream") {
|
|
174
|
+
if (contentType) {
|
|
175
|
+
existing.contentType = contentType;
|
|
176
|
+
}
|
|
177
|
+
return existing;
|
|
178
|
+
}
|
|
179
|
+
const body = {
|
|
180
|
+
$type: "stream",
|
|
181
|
+
contentType: contentType ?? "application/octet-stream",
|
|
182
|
+
bytes: 0,
|
|
183
|
+
};
|
|
184
|
+
context.responseBody = body;
|
|
185
|
+
return body;
|
|
186
|
+
}
|
|
187
|
+
function isTextLikeStream(contentType) {
|
|
188
|
+
return (contentType.includes("text/") ||
|
|
189
|
+
contentType.includes("json") ||
|
|
190
|
+
contentType.includes("xml") ||
|
|
191
|
+
contentType.includes("event-stream"));
|
|
192
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRequestStatsMiddleware = registerRequestStatsMiddleware;
|
|
4
|
+
exports.setStatsPayload = setStatsPayload;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const statsRepository_1 = require("../storage/statsRepository");
|
|
7
|
+
// WeakMap to store request context without polluting request object
|
|
8
|
+
const requestContexts = new WeakMap();
|
|
9
|
+
async function registerRequestStatsMiddleware(app, paths) {
|
|
10
|
+
// Decorate request with stats context
|
|
11
|
+
app.decorateRequest("statsContext", null);
|
|
12
|
+
// Hook: onRequest - capture start time and request size
|
|
13
|
+
app.addHook("onRequest", async (req) => {
|
|
14
|
+
// Only track /v1/* routes
|
|
15
|
+
if (!req.url.startsWith("/v1/")) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const context = {
|
|
19
|
+
requestId: (0, crypto_1.randomUUID)(),
|
|
20
|
+
startTime: Date.now(),
|
|
21
|
+
requestBytes: 0,
|
|
22
|
+
route: req.url,
|
|
23
|
+
method: req.method
|
|
24
|
+
};
|
|
25
|
+
// Estimate request size from content-length header
|
|
26
|
+
const contentLength = req.headers["content-length"];
|
|
27
|
+
if (contentLength) {
|
|
28
|
+
context.requestBytes = parseInt(contentLength, 10) || 0;
|
|
29
|
+
}
|
|
30
|
+
requestContexts.set(req, context);
|
|
31
|
+
});
|
|
32
|
+
// Hook: preHandler - extract model from parsed body
|
|
33
|
+
app.addHook("preHandler", async (req) => {
|
|
34
|
+
const context = requestContexts.get(req);
|
|
35
|
+
if (!context)
|
|
36
|
+
return;
|
|
37
|
+
const body = req.body;
|
|
38
|
+
if (body?.model) {
|
|
39
|
+
context.publicModel = body.model;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Hook: onResponse - log the stats
|
|
43
|
+
app.addHook("onResponse", async (req, reply) => {
|
|
44
|
+
const context = requestContexts.get(req);
|
|
45
|
+
if (!context)
|
|
46
|
+
return;
|
|
47
|
+
const latencyMs = Date.now() - context.startTime;
|
|
48
|
+
const statusCode = reply.statusCode;
|
|
49
|
+
// Try to get response size from content-length header
|
|
50
|
+
let responseBytes = 0;
|
|
51
|
+
const respContentLength = reply.getHeader("content-length");
|
|
52
|
+
if (respContentLength) {
|
|
53
|
+
responseBytes = typeof respContentLength === "number"
|
|
54
|
+
? respContentLength
|
|
55
|
+
: parseInt(String(respContentLength), 10) || 0;
|
|
56
|
+
}
|
|
57
|
+
// Determine if there was an error
|
|
58
|
+
let errorType;
|
|
59
|
+
if (statusCode >= 400) {
|
|
60
|
+
if (statusCode >= 500) {
|
|
61
|
+
errorType = "server_error";
|
|
62
|
+
}
|
|
63
|
+
else if (statusCode === 429) {
|
|
64
|
+
errorType = "rate_limit";
|
|
65
|
+
}
|
|
66
|
+
else if (statusCode === 401 || statusCode === 403) {
|
|
67
|
+
errorType = "auth_error";
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
errorType = "client_error";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Extract token info from reply (if stored during route handling)
|
|
74
|
+
const statsPayload = reply.statsPayload;
|
|
75
|
+
const stats = {
|
|
76
|
+
requestId: context.requestId,
|
|
77
|
+
timestamp: new Date(),
|
|
78
|
+
route: context.route,
|
|
79
|
+
method: context.method,
|
|
80
|
+
publicModel: context.publicModel,
|
|
81
|
+
endpointId: statsPayload?.endpointId,
|
|
82
|
+
endpointName: statsPayload?.endpointName,
|
|
83
|
+
upstreamModel: statsPayload?.upstreamModel,
|
|
84
|
+
requestBytes: context.requestBytes,
|
|
85
|
+
responseBytes,
|
|
86
|
+
latencyMs,
|
|
87
|
+
statusCode,
|
|
88
|
+
errorType,
|
|
89
|
+
totalTokens: statsPayload?.totalTokens ?? estimateTokens(context.requestBytes, responseBytes),
|
|
90
|
+
promptTokens: statsPayload?.promptTokens ?? null,
|
|
91
|
+
completionTokens: statsPayload?.completionTokens ?? null
|
|
92
|
+
};
|
|
93
|
+
// Append stats asynchronously (don't block response)
|
|
94
|
+
(0, statsRepository_1.appendStats)(paths, stats).catch((err) => {
|
|
95
|
+
app.log.error({ err }, "Failed to append request stats");
|
|
96
|
+
});
|
|
97
|
+
// Cleanup
|
|
98
|
+
requestContexts.delete(req);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Helper to set stats payload from route handlers
|
|
103
|
+
*/
|
|
104
|
+
function setStatsPayload(reply, payload) {
|
|
105
|
+
reply.statsPayload = payload;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Estimate token count from byte sizes when actual usage is not available.
|
|
109
|
+
* Uses rough approximation: ~4 characters per token, ~1 byte per character for English.
|
|
110
|
+
* This is intentionally conservative.
|
|
111
|
+
*/
|
|
112
|
+
function estimateTokens(requestBytes, responseBytes) {
|
|
113
|
+
if (requestBytes === 0 && responseBytes === 0) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// Rough estimate: 4 bytes per token average
|
|
117
|
+
return Math.ceil((requestBytes + responseBytes) / 4);
|
|
118
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rebuildDefaultPools = rebuildDefaultPools;
|
|
4
|
+
const repository_1 = require("../providers/repository");
|
|
5
|
+
const modelCapabilities_1 = require("../utils/modelCapabilities");
|
|
6
|
+
const repository_2 = require("./repository");
|
|
7
|
+
const repository_3 = require("../providers/repository");
|
|
8
|
+
const DEFAULT_SCORE_FALLBACK = 20;
|
|
9
|
+
const DEFAULT_POOLS = [
|
|
10
|
+
{
|
|
11
|
+
id: "smart",
|
|
12
|
+
aliases: ["smart"],
|
|
13
|
+
requiredInput: [],
|
|
14
|
+
requiredOutput: [],
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
function buildCandidate(provider, model, scoreFallback) {
|
|
18
|
+
const effectiveBaseUrl = model.baseUrl ?? provider.baseUrl;
|
|
19
|
+
const score = model.benchmark?.livebench;
|
|
20
|
+
return {
|
|
21
|
+
id: model.providerModelId,
|
|
22
|
+
providerModelId: model.providerModelId,
|
|
23
|
+
providerId: provider.id,
|
|
24
|
+
providerName: provider.name,
|
|
25
|
+
providerEnabled: provider.enabled,
|
|
26
|
+
modelEnabled: model.enabled !== false,
|
|
27
|
+
modelId: model.modelId,
|
|
28
|
+
aliases: model.aliases ?? [],
|
|
29
|
+
upstreamModel: model.upstreamModel,
|
|
30
|
+
baseUrl: effectiveBaseUrl ?? "",
|
|
31
|
+
apiKey: model.apiKey ?? provider.apiKey,
|
|
32
|
+
insecureTls: (0, repository_3.getEffectiveModelInsecureTls)(provider, model),
|
|
33
|
+
autoInsecureTlsDomains: provider.autoInsecureTlsDomains ?? [],
|
|
34
|
+
protocol: provider.protocol,
|
|
35
|
+
protocolConfig: provider.protocolConfig,
|
|
36
|
+
auth: provider.auth,
|
|
37
|
+
supportsRouting: provider.supportsRouting,
|
|
38
|
+
free: model.free,
|
|
39
|
+
endpointType: model.endpointType,
|
|
40
|
+
capabilities: model.capabilities,
|
|
41
|
+
score: typeof score === "number" ? score : scoreFallback,
|
|
42
|
+
scoreSource: typeof score === "number" ? "benchmark.livebench" : "fallback",
|
|
43
|
+
limits: {
|
|
44
|
+
requestsPerMinute: model.limits?.requests?.perMinute ?? provider.limits?.requests?.perMinute,
|
|
45
|
+
requestsPerHour: model.limits?.requests?.perHour ?? provider.limits?.requests?.perHour,
|
|
46
|
+
requestsPerDay: model.limits?.requests?.perDay ?? provider.limits?.requests?.perDay,
|
|
47
|
+
requestsPerWeek: model.limits?.requests?.perWeek ?? provider.limits?.requests?.perWeek,
|
|
48
|
+
tokensPerMinute: model.limits?.tokens?.perMinute ?? provider.limits?.tokens?.perMinute,
|
|
49
|
+
tokensPerHour: model.limits?.tokens?.perHour ?? provider.limits?.tokens?.perHour,
|
|
50
|
+
tokensPerDay: model.limits?.tokens?.perDay ?? provider.limits?.tokens?.perDay,
|
|
51
|
+
tokensPerWeek: model.limits?.tokens?.perWeek ?? provider.limits?.tokens?.perWeek,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function rebuildDefaultPools(paths, scoreFallback = DEFAULT_SCORE_FALLBACK) {
|
|
56
|
+
const existing = await (0, repository_2.loadPools)(paths);
|
|
57
|
+
const providers = await (0, repository_1.listProviders)(paths);
|
|
58
|
+
const userPools = existing.pools.filter((p) => p.userDefined);
|
|
59
|
+
const autoPools = existing.pools.filter((p) => !p.userDefined);
|
|
60
|
+
const autoPoolIds = new Set(autoPools.map((p) => p.id));
|
|
61
|
+
const pools = DEFAULT_POOLS.map((template) => {
|
|
62
|
+
const existingAuto = autoPools.find((p) => p.id === template.id);
|
|
63
|
+
return {
|
|
64
|
+
id: template.id,
|
|
65
|
+
name: existingAuto?.name ?? template.id,
|
|
66
|
+
aliases: template.aliases,
|
|
67
|
+
enabled: existingAuto?.enabled ?? true,
|
|
68
|
+
strategy: existingAuto?.strategy ?? "highest_rank_available",
|
|
69
|
+
requiredInput: template.requiredInput,
|
|
70
|
+
requiredOutput: template.requiredOutput,
|
|
71
|
+
scoreFallback,
|
|
72
|
+
candidates: [],
|
|
73
|
+
candidateSelection: existingAuto?.candidateSelection ?? [],
|
|
74
|
+
userDefined: false,
|
|
75
|
+
updatedAt: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const allCandidates = new Map();
|
|
79
|
+
for (const provider of providers) {
|
|
80
|
+
for (const model of provider.models) {
|
|
81
|
+
const candidate = buildCandidate(provider, model, scoreFallback);
|
|
82
|
+
allCandidates.set(`${provider.id}/${model.modelId}`, candidate);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const pool of pools) {
|
|
86
|
+
for (const [key, candidate] of allCandidates) {
|
|
87
|
+
if (!candidate.modelEnabled || !candidate.providerEnabled)
|
|
88
|
+
continue;
|
|
89
|
+
if (!candidate.free && !pool.userDefined)
|
|
90
|
+
continue;
|
|
91
|
+
if ((0, modelCapabilities_1.supportsRequirements)(candidate.capabilities, {
|
|
92
|
+
requiredInput: pool.requiredInput,
|
|
93
|
+
requiredOutput: pool.requiredOutput,
|
|
94
|
+
})) {
|
|
95
|
+
pool.candidates.push(candidate);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (pool.candidateSelection.length > 0) {
|
|
99
|
+
const selected = pool.candidateSelection
|
|
100
|
+
.map((sel) => allCandidates.get(sel))
|
|
101
|
+
.filter((c) => c !== undefined);
|
|
102
|
+
if (selected.length > 0) {
|
|
103
|
+
pool.candidates = selected;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
pool.candidates = pool.candidates.sort((a, b) => {
|
|
107
|
+
if (b.score !== a.score) {
|
|
108
|
+
return b.score - a.score;
|
|
109
|
+
}
|
|
110
|
+
return `${a.providerId}/${a.modelId}`.localeCompare(`${b.providerId}/${b.modelId}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
for (const userPool of userPools) {
|
|
114
|
+
if (!allPoolsHasId(pools, userPool.id)) {
|
|
115
|
+
userPool.candidates = (userPool.candidateSelection ?? [])
|
|
116
|
+
.map((sel) => allCandidates.get(sel))
|
|
117
|
+
.filter((c) => c !== undefined);
|
|
118
|
+
userPool.candidates = userPool.candidates.sort((a, b) => {
|
|
119
|
+
if (b.score !== a.score)
|
|
120
|
+
return b.score - a.score;
|
|
121
|
+
return `${a.providerId}/${a.modelId}`.localeCompare(`${b.providerId}/${b.modelId}`);
|
|
122
|
+
});
|
|
123
|
+
userPool.updatedAt = new Date().toISOString();
|
|
124
|
+
pools.push(userPool);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await (0, repository_2.savePools)(paths, pools);
|
|
128
|
+
return pools;
|
|
129
|
+
}
|
|
130
|
+
function allPoolsHasId(pools, id) {
|
|
131
|
+
return pools.some((p) => p.id === id);
|
|
132
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadPools = loadPools;
|
|
4
|
+
exports.savePools = savePools;
|
|
5
|
+
exports.listPools = listPools;
|
|
6
|
+
exports.getPoolByAlias = getPoolByAlias;
|
|
7
|
+
exports.loadPoolState = loadPoolState;
|
|
8
|
+
exports.savePoolState = savePoolState;
|
|
9
|
+
const files_1 = require("../storage/files");
|
|
10
|
+
const POOLS_VERSION = 1;
|
|
11
|
+
const STATE_VERSION = 1;
|
|
12
|
+
function defaultPoolStore() {
|
|
13
|
+
return {
|
|
14
|
+
version: POOLS_VERSION,
|
|
15
|
+
updatedAt: new Date().toISOString(),
|
|
16
|
+
pools: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function defaultPoolState() {
|
|
20
|
+
return {
|
|
21
|
+
version: STATE_VERSION,
|
|
22
|
+
updatedAt: new Date().toISOString(),
|
|
23
|
+
candidates: {},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function loadPools(paths) {
|
|
27
|
+
const store = await (0, files_1.readJsonFile)(paths.poolsPath, defaultPoolStore());
|
|
28
|
+
if (!Array.isArray(store.pools)) {
|
|
29
|
+
return defaultPoolStore();
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
version: Number.isFinite(store.version) ? store.version : POOLS_VERSION,
|
|
33
|
+
updatedAt: typeof store.updatedAt === "string" ? store.updatedAt : new Date().toISOString(),
|
|
34
|
+
pools: store.pools,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function savePools(paths, pools) {
|
|
38
|
+
await (0, files_1.writeJsonFile)(paths.poolsPath, {
|
|
39
|
+
version: POOLS_VERSION,
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
pools,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function listPools(paths) {
|
|
45
|
+
const store = await loadPools(paths);
|
|
46
|
+
return [...store.pools].sort((a, b) => a.id.localeCompare(b.id));
|
|
47
|
+
}
|
|
48
|
+
async function getPoolByAlias(paths, alias) {
|
|
49
|
+
const pools = await listPools(paths);
|
|
50
|
+
return pools.find((pool) => pool.aliases.includes(alias)) ?? null;
|
|
51
|
+
}
|
|
52
|
+
async function loadPoolState(paths) {
|
|
53
|
+
const state = await (0, files_1.readJsonFile)(paths.poolStatePath, defaultPoolState());
|
|
54
|
+
if (!state.candidates || typeof state.candidates !== "object") {
|
|
55
|
+
return defaultPoolState();
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
version: Number.isFinite(state.version) ? state.version : STATE_VERSION,
|
|
59
|
+
updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : new Date().toISOString(),
|
|
60
|
+
candidates: state.candidates,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function savePoolState(paths, state) {
|
|
64
|
+
await (0, files_1.writeJsonFile)(paths.poolStatePath, {
|
|
65
|
+
...state,
|
|
66
|
+
version: STATE_VERSION,
|
|
67
|
+
updatedAt: new Date().toISOString(),
|
|
68
|
+
});
|
|
69
|
+
}
|