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,293 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { StoragePaths } from "../storage/files";
|
|
3
|
+
import { getMediaEntry, getMediaPath } from "../storage/imageCache";
|
|
4
|
+
|
|
5
|
+
export interface MessageMediaScan {
|
|
6
|
+
hasImage: boolean;
|
|
7
|
+
hasAudio: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function scanMessageModalities(messages: unknown): MessageMediaScan {
|
|
11
|
+
const result: MessageMediaScan = { hasImage: false, hasAudio: false };
|
|
12
|
+
if (!Array.isArray(messages)) {
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const message of messages) {
|
|
17
|
+
if (!message || typeof message !== "object") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const content = (message as { content?: unknown }).content;
|
|
21
|
+
if (!Array.isArray(content)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
for (const part of content) {
|
|
25
|
+
if (!part || typeof part !== "object") {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const type = (part as { type?: unknown }).type;
|
|
29
|
+
if (type === "image_url" || type === "input_image" || type === "image") {
|
|
30
|
+
result.hasImage = true;
|
|
31
|
+
}
|
|
32
|
+
if (type === "input_audio" || type === "audio") {
|
|
33
|
+
result.hasAudio = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function normalizeMessagesForUpstream(
|
|
42
|
+
paths: StoragePaths,
|
|
43
|
+
messages: unknown
|
|
44
|
+
): Promise<unknown> {
|
|
45
|
+
if (!Array.isArray(messages)) {
|
|
46
|
+
return messages;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const normalized: unknown[] = [];
|
|
50
|
+
for (const message of messages) {
|
|
51
|
+
if (!message || typeof message !== "object") {
|
|
52
|
+
normalized.push(message);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const nextMessage: Record<string, unknown> = { ...(message as Record<string, unknown>) };
|
|
57
|
+
const content = nextMessage.content;
|
|
58
|
+
if (!Array.isArray(content)) {
|
|
59
|
+
normalized.push(nextMessage);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nextContent: unknown[] = [];
|
|
64
|
+
for (const rawPart of content) {
|
|
65
|
+
if (!rawPart || typeof rawPart !== "object") {
|
|
66
|
+
nextContent.push(rawPart);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const part = { ...(rawPart as Record<string, unknown>) };
|
|
70
|
+
const type = part.type;
|
|
71
|
+
|
|
72
|
+
if (type === "video") {
|
|
73
|
+
throw invalidRequestError("Video content is not supported in v1 omni mode.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (type === "image" && typeof part.image === "string") {
|
|
77
|
+
nextContent.push({ type: "image_url", image_url: { url: part.image } });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (type === "image_url") {
|
|
82
|
+
nextContent.push(await normalizeImageUrlPart(paths, part));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (type === "audio" && typeof part.audio === "string") {
|
|
87
|
+
nextContent.push(await normalizeAudioValue(paths, part.audio));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (type === "input_audio") {
|
|
92
|
+
nextContent.push(await normalizeInputAudioPart(paths, part));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
nextContent.push(part);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
nextMessage.content = nextContent;
|
|
100
|
+
normalized.push(nextMessage);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function normalizeImageUrlPart(
|
|
107
|
+
paths: StoragePaths,
|
|
108
|
+
part: Record<string, unknown>
|
|
109
|
+
): Promise<Record<string, unknown>> {
|
|
110
|
+
const imageUrlObject = (part.image_url ?? {}) as { url?: unknown };
|
|
111
|
+
const value = imageUrlObject.url;
|
|
112
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
113
|
+
return part;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (value.startsWith("data:")) {
|
|
117
|
+
return part;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hash = extractLocalHash(value);
|
|
121
|
+
if (!hash) {
|
|
122
|
+
return part;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const mediaPath = await getMediaPath(paths, hash);
|
|
126
|
+
const mediaEntry = await getMediaEntry(paths, hash);
|
|
127
|
+
if (!mediaPath || !mediaEntry) {
|
|
128
|
+
throw invalidRequestError("Referenced image not found in cache.");
|
|
129
|
+
}
|
|
130
|
+
const file = await import("fs/promises");
|
|
131
|
+
const buffer = await file.readFile(mediaPath);
|
|
132
|
+
const dataUrl = `data:${mediaEntry.mimeType};base64,${buffer.toString("base64")}`;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...part,
|
|
136
|
+
type: "image_url",
|
|
137
|
+
image_url: {
|
|
138
|
+
...imageUrlObject,
|
|
139
|
+
url: dataUrl,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function normalizeInputAudioPart(
|
|
145
|
+
paths: StoragePaths,
|
|
146
|
+
part: Record<string, unknown>
|
|
147
|
+
): Promise<Record<string, unknown>> {
|
|
148
|
+
const inputAudio = (part.input_audio ?? {}) as Record<string, unknown>;
|
|
149
|
+
const data = inputAudio.data;
|
|
150
|
+
const url = inputAudio.url;
|
|
151
|
+
|
|
152
|
+
if (typeof data === "string" && data.length > 0) {
|
|
153
|
+
return {
|
|
154
|
+
...part,
|
|
155
|
+
type: "input_audio",
|
|
156
|
+
input_audio: {
|
|
157
|
+
...inputAudio,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (typeof url === "string" && url.length > 0) {
|
|
163
|
+
const resolved = await resolveLocalMediaUrl(paths, url);
|
|
164
|
+
return {
|
|
165
|
+
...part,
|
|
166
|
+
type: "input_audio",
|
|
167
|
+
input_audio: resolved,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw invalidRequestError("input_audio requires either data or a local media url.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function normalizeAudioValue(paths: StoragePaths, value: string): Promise<Record<string, unknown>> {
|
|
175
|
+
if (value.startsWith("data:")) {
|
|
176
|
+
const parsed = parseDataUrl(value);
|
|
177
|
+
return {
|
|
178
|
+
type: "input_audio",
|
|
179
|
+
input_audio: {
|
|
180
|
+
data: parsed.base64,
|
|
181
|
+
format: parsed.format,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (looksLikeBase64(value)) {
|
|
187
|
+
return {
|
|
188
|
+
type: "input_audio",
|
|
189
|
+
input_audio: {
|
|
190
|
+
data: value,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const resolved = await resolveLocalMediaUrl(paths, value);
|
|
196
|
+
return {
|
|
197
|
+
type: "input_audio",
|
|
198
|
+
input_audio: resolved,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function resolveLocalMediaUrl(
|
|
203
|
+
paths: StoragePaths,
|
|
204
|
+
url: string
|
|
205
|
+
): Promise<{ data: string; format?: string }> {
|
|
206
|
+
const hash = extractLocalHash(url);
|
|
207
|
+
if (!hash) {
|
|
208
|
+
throw invalidRequestError("Only local /admin/media or /admin/images URLs are allowed for input_audio.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const mediaPath = await getMediaPath(paths, hash);
|
|
212
|
+
const mediaEntry = await getMediaEntry(paths, hash);
|
|
213
|
+
if (!mediaPath || !mediaEntry) {
|
|
214
|
+
throw invalidRequestError("Referenced media not found in cache.");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const file = await import("fs/promises");
|
|
218
|
+
const buffer = await file.readFile(mediaPath);
|
|
219
|
+
return {
|
|
220
|
+
data: buffer.toString("base64"),
|
|
221
|
+
format: audioFormatFromMime(mediaEntry.mimeType, mediaPath),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractLocalHash(url: string): string | null {
|
|
226
|
+
const normalized = normalizeLocalUrl(url);
|
|
227
|
+
if (!normalized) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const mediaMatch = normalized.match(/^\/admin\/(media|images)\/([a-f0-9]{16})$/i);
|
|
231
|
+
if (!mediaMatch) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return mediaMatch[2];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeLocalUrl(url: string): string | null {
|
|
238
|
+
if (url.startsWith("/")) {
|
|
239
|
+
return url;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const parsed = new URL(url);
|
|
244
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
if (!["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return parsed.pathname;
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseDataUrl(value: string): { base64: string; format?: string } {
|
|
257
|
+
const match = value.match(/^data:([^;]+);base64,(.+)$/i);
|
|
258
|
+
if (!match) {
|
|
259
|
+
throw invalidRequestError("Invalid data URL for audio input.");
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
base64: match[2].replace(/\s+/g, ""),
|
|
263
|
+
format: audioFormatFromMime(match[1]),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function audioFormatFromMime(mimeType: string, filePath?: string): string | undefined {
|
|
268
|
+
const lower = mimeType.toLowerCase();
|
|
269
|
+
if (lower.includes("wav")) return "wav";
|
|
270
|
+
if (lower.includes("mpeg") || lower.includes("mp3")) return "mp3";
|
|
271
|
+
if (lower.includes("ogg")) return "ogg";
|
|
272
|
+
if (lower.includes("webm")) return "webm";
|
|
273
|
+
if (lower.includes("mp4") || lower.includes("m4a")) return "m4a";
|
|
274
|
+
if (filePath) {
|
|
275
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
276
|
+
if (ext) return ext;
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function looksLikeBase64(value: string): boolean {
|
|
282
|
+
if (value.length < 32) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return /^[A-Za-z0-9+/=\s]+$/.test(value);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function invalidRequestError(message: string): Error & { type: string; retryable: boolean } {
|
|
289
|
+
const error = new Error(message) as Error & { type: string; retryable: boolean };
|
|
290
|
+
error.type = "invalid_request";
|
|
291
|
+
error.retryable = false;
|
|
292
|
+
return error;
|
|
293
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EndpointType,
|
|
3
|
+
ModelCapabilities,
|
|
4
|
+
ModelMapping,
|
|
5
|
+
ModelModality,
|
|
6
|
+
} from "../types";
|
|
7
|
+
|
|
8
|
+
export interface CapabilitiesRequirements {
|
|
9
|
+
requiredInput?: ModelModality[];
|
|
10
|
+
requiredOutput?: ModelModality[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveCapabilities(
|
|
14
|
+
mapping: ModelMapping,
|
|
15
|
+
endpointType: EndpointType,
|
|
16
|
+
upstreamCaps?: ModelCapabilities
|
|
17
|
+
): ModelCapabilities {
|
|
18
|
+
if (mapping.capabilities) {
|
|
19
|
+
return normalizeCapabilities(mapping.capabilities, "configured");
|
|
20
|
+
}
|
|
21
|
+
if (upstreamCaps) {
|
|
22
|
+
return normalizeCapabilities(upstreamCaps, "inferred");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const inferred = inferCapabilities(mapping.publicName, endpointType);
|
|
26
|
+
warnInference(mapping.publicName, endpointType, inferred);
|
|
27
|
+
return normalizeCapabilities(inferred, "inferred");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function inferCapabilities(
|
|
31
|
+
modelName: string,
|
|
32
|
+
endpointType: EndpointType
|
|
33
|
+
): ModelCapabilities {
|
|
34
|
+
const name = modelName.toLowerCase();
|
|
35
|
+
|
|
36
|
+
if (endpointType === "embedding") {
|
|
37
|
+
return { input: ["text"], output: ["embedding"] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (endpointType === "diffusion") {
|
|
41
|
+
return { input: ["text"], output: ["image"] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (endpointType === "audio") {
|
|
45
|
+
if (isTtsModelName(name)) {
|
|
46
|
+
return { input: ["text"], output: ["audio"] };
|
|
47
|
+
}
|
|
48
|
+
return { input: ["audio"], output: ["text"] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (endpointType === "video") {
|
|
52
|
+
if (isImageToVideoModelName(name)) {
|
|
53
|
+
return { input: ["text", "image"], output: ["video"] };
|
|
54
|
+
}
|
|
55
|
+
return { input: ["text"], output: ["video"] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isVisionModelName(name)) {
|
|
59
|
+
return { input: ["text", "image"], output: ["text"], supportsTools: true, supportsStreaming: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { input: ["text"], output: ["text"], supportsTools: true, supportsStreaming: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function supportsRequirements(
|
|
66
|
+
capabilities: ModelCapabilities,
|
|
67
|
+
requirements?: CapabilitiesRequirements
|
|
68
|
+
): boolean {
|
|
69
|
+
if (!requirements) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (requirements.requiredInput && requirements.requiredInput.length > 0) {
|
|
74
|
+
for (const modality of requirements.requiredInput) {
|
|
75
|
+
if (!capabilities.input.includes(modality)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (requirements.requiredOutput && requirements.requiredOutput.length > 0) {
|
|
82
|
+
for (const modality of requirements.requiredOutput) {
|
|
83
|
+
if (!capabilities.output.includes(modality)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeCapabilities(
|
|
93
|
+
capabilities: ModelCapabilities,
|
|
94
|
+
source: "configured" | "inferred"
|
|
95
|
+
): ModelCapabilities {
|
|
96
|
+
return {
|
|
97
|
+
input: normalizeModalities(capabilities.input),
|
|
98
|
+
output: normalizeModalities(capabilities.output),
|
|
99
|
+
supportsTools: capabilities.supportsTools,
|
|
100
|
+
supportsStreaming: capabilities.supportsStreaming,
|
|
101
|
+
source,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeModalities(modalities: ModelModality[]): ModelModality[] {
|
|
106
|
+
const allowed: ModelModality[] = ["text", "image", "audio", "embedding", "video"];
|
|
107
|
+
const unique = new Set<ModelModality>();
|
|
108
|
+
|
|
109
|
+
for (const modality of modalities) {
|
|
110
|
+
if (allowed.includes(modality)) {
|
|
111
|
+
unique.add(modality);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return allowed.filter((modality) => unique.has(modality));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isTtsModelName(name: string): boolean {
|
|
119
|
+
return (
|
|
120
|
+
name.includes("tts") ||
|
|
121
|
+
name.includes("speech") ||
|
|
122
|
+
name.includes("voice") ||
|
|
123
|
+
name.includes("audio-gen")
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isVisionModelName(name: string): boolean {
|
|
128
|
+
return (
|
|
129
|
+
name.includes("vision") ||
|
|
130
|
+
name.includes("vl") ||
|
|
131
|
+
name.includes("omni") ||
|
|
132
|
+
name.includes("multimodal")
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isImageToVideoModelName(name: string): boolean {
|
|
137
|
+
return (
|
|
138
|
+
name.includes("i2v") ||
|
|
139
|
+
name.includes("image-to-video") ||
|
|
140
|
+
name.includes("img2vid") ||
|
|
141
|
+
name.includes("kf2v")
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const capabilityInferenceWarnings = new Set<string>();
|
|
146
|
+
|
|
147
|
+
function warnInference(
|
|
148
|
+
modelName: string,
|
|
149
|
+
endpointType: EndpointType,
|
|
150
|
+
capabilities: ModelCapabilities
|
|
151
|
+
): void {
|
|
152
|
+
const key = `${endpointType}:${modelName}`;
|
|
153
|
+
if (capabilityInferenceWarnings.has(key)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
capabilityInferenceWarnings.add(key);
|
|
157
|
+
console.warn(
|
|
158
|
+
`[waypoi] Inferred capabilities for model '${modelName}' on ${endpointType}: ` +
|
|
159
|
+
`${capabilities.input.join("+")}->${capabilities.output.join("+")}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Agent, request } from "undici";
|
|
2
|
+
import { ModelCapabilities, ModelMapping, ModelModality } from "../types";
|
|
3
|
+
import { ProviderAuthConfig } from "../providers/types";
|
|
4
|
+
|
|
5
|
+
export interface EndpointInfo {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
insecureTls: boolean;
|
|
9
|
+
auth?: ProviderAuthConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function resolveModelMappings(
|
|
13
|
+
endpoint: EndpointInfo,
|
|
14
|
+
mappings: ModelMapping[]
|
|
15
|
+
): Promise<ModelMapping[]> {
|
|
16
|
+
let models: UpstreamModelInfo[] | null = null;
|
|
17
|
+
try {
|
|
18
|
+
models = await discoverUpstreamModels(endpoint);
|
|
19
|
+
} catch {
|
|
20
|
+
models = null;
|
|
21
|
+
}
|
|
22
|
+
if (!models || models.length === 0) {
|
|
23
|
+
return mappings;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If exactly one model, use it as the upstream for all mappings
|
|
27
|
+
// that don't already have an explicit upstream different from the public name
|
|
28
|
+
if (models.length === 1) {
|
|
29
|
+
const sole = models[0];
|
|
30
|
+
console.log(`[model-discovery] Single model found: ${sole.id}`);
|
|
31
|
+
return mappings.map((mapping) => {
|
|
32
|
+
// If user specified explicit upstream (public=upstream format), keep it
|
|
33
|
+
// Otherwise, use the discovered model as upstream
|
|
34
|
+
if (mapping.publicName !== mapping.upstreamModel) {
|
|
35
|
+
// User specified explicit mapping, keep it
|
|
36
|
+
return mapping;
|
|
37
|
+
}
|
|
38
|
+
// Public and upstream are the same (user just gave public name)
|
|
39
|
+
// Replace upstream with discovered model
|
|
40
|
+
console.log(`[model-discovery] Mapping ${mapping.publicName} -> ${sole.id}`);
|
|
41
|
+
return {
|
|
42
|
+
...mapping,
|
|
43
|
+
upstreamModel: sole.id,
|
|
44
|
+
capabilities: mapping.capabilities ?? sole.capabilities,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Multiple models - check if any mapping's upstream matches available models
|
|
50
|
+
console.log(
|
|
51
|
+
`[model-discovery] ${models.length} models found: ${models
|
|
52
|
+
.slice(0, 5)
|
|
53
|
+
.map((model) => model.id)
|
|
54
|
+
.join(", ")}${models.length > 5 ? "..." : ""}`
|
|
55
|
+
);
|
|
56
|
+
const byId = new Map(models.map((model) => [model.id, model]));
|
|
57
|
+
return mappings.map((mapping) => {
|
|
58
|
+
if (mapping.capabilities) {
|
|
59
|
+
return mapping;
|
|
60
|
+
}
|
|
61
|
+
const matched = byId.get(mapping.upstreamModel) ?? byId.get(mapping.publicName);
|
|
62
|
+
if (!matched?.capabilities) {
|
|
63
|
+
return mapping;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...mapping,
|
|
67
|
+
capabilities: matched.capabilities,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface UpstreamModelInfo {
|
|
73
|
+
id: string;
|
|
74
|
+
capabilities?: ModelCapabilities;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function discoverUpstreamModels(endpoint: EndpointInfo): Promise<UpstreamModelInfo[]> {
|
|
78
|
+
const dispatcher = endpoint.insecureTls
|
|
79
|
+
? new Agent({ connect: { rejectUnauthorized: false } })
|
|
80
|
+
: undefined;
|
|
81
|
+
const { url, headers } = buildDiscoveryRequest(endpoint);
|
|
82
|
+
const response = await request(url, {
|
|
83
|
+
method: "GET",
|
|
84
|
+
headersTimeout: 3000,
|
|
85
|
+
bodyTimeout: 3000,
|
|
86
|
+
dispatcher,
|
|
87
|
+
headers,
|
|
88
|
+
});
|
|
89
|
+
const body = (await readJson(response.body)) as {
|
|
90
|
+
data?: Array<{
|
|
91
|
+
id?: string;
|
|
92
|
+
input_modalities?: string[];
|
|
93
|
+
output_modalities?: string[];
|
|
94
|
+
capabilities?: { input?: string[]; output?: string[]; supportsTools?: boolean; supportsStreaming?: boolean };
|
|
95
|
+
}>;
|
|
96
|
+
} | null;
|
|
97
|
+
response.body.resume();
|
|
98
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
99
|
+
throw new Error(`model discovery failed with status ${response.statusCode}`);
|
|
100
|
+
}
|
|
101
|
+
const list = Array.isArray(body?.data) ? body.data : [];
|
|
102
|
+
const models: UpstreamModelInfo[] = [];
|
|
103
|
+
for (const item of list) {
|
|
104
|
+
if (!item.id) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const modelInfo: UpstreamModelInfo = { id: item.id };
|
|
108
|
+
const capabilities = extractCapabilities(item);
|
|
109
|
+
if (capabilities) {
|
|
110
|
+
modelInfo.capabilities = capabilities;
|
|
111
|
+
}
|
|
112
|
+
models.push(modelInfo);
|
|
113
|
+
}
|
|
114
|
+
return models;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildDiscoveryRequest(endpoint: EndpointInfo): { url: string; headers: Record<string, string> } {
|
|
118
|
+
const authType = endpoint.auth?.type ?? "bearer";
|
|
119
|
+
const headers: Record<string, string> = {};
|
|
120
|
+
const url = new URL(buildModelListUrl(endpoint.baseUrl));
|
|
121
|
+
const apiKey = endpoint.apiKey?.trim();
|
|
122
|
+
|
|
123
|
+
if (apiKey && authType === "query") {
|
|
124
|
+
const keyParam = endpoint.auth?.keyParam?.trim() || "api_key";
|
|
125
|
+
url.searchParams.set(keyParam, apiKey);
|
|
126
|
+
} else if (apiKey && authType === "header") {
|
|
127
|
+
const headerName = endpoint.auth?.headerName?.trim() || endpoint.auth?.keyParam?.trim() || "x-api-key";
|
|
128
|
+
const prefix = endpoint.auth?.keyPrefix?.trim();
|
|
129
|
+
headers[headerName] = prefix ? `${prefix} ${apiKey}` : apiKey;
|
|
130
|
+
} else if (apiKey && authType !== "none") {
|
|
131
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { url: url.toString(), headers };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildModelListUrl(baseUrl: string): string {
|
|
138
|
+
const parsed = new URL(baseUrl);
|
|
139
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
140
|
+
if (!pathname) {
|
|
141
|
+
parsed.pathname = "/v1/models";
|
|
142
|
+
} else if (pathname.endsWith("/v1")) {
|
|
143
|
+
parsed.pathname = `${pathname}/models`;
|
|
144
|
+
} else {
|
|
145
|
+
parsed.pathname = `${pathname}/v1/models`;
|
|
146
|
+
}
|
|
147
|
+
return parsed.toString();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractCapabilities(item: {
|
|
151
|
+
input_modalities?: string[];
|
|
152
|
+
output_modalities?: string[];
|
|
153
|
+
capabilities?: { input?: string[]; output?: string[]; supportsTools?: boolean; supportsStreaming?: boolean };
|
|
154
|
+
}): ModelCapabilities | undefined {
|
|
155
|
+
const fromCapabilities = item.capabilities;
|
|
156
|
+
if (fromCapabilities?.input && fromCapabilities?.output) {
|
|
157
|
+
const input = normalizeModalities(fromCapabilities.input);
|
|
158
|
+
const output = normalizeModalities(fromCapabilities.output);
|
|
159
|
+
if (input.length > 0 && output.length > 0) {
|
|
160
|
+
return {
|
|
161
|
+
input,
|
|
162
|
+
output,
|
|
163
|
+
supportsTools: fromCapabilities.supportsTools,
|
|
164
|
+
supportsStreaming: fromCapabilities.supportsStreaming,
|
|
165
|
+
source: "inferred",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const input = normalizeModalities(item.input_modalities ?? []);
|
|
171
|
+
const output = normalizeModalities(item.output_modalities ?? []);
|
|
172
|
+
if (input.length > 0 && output.length > 0) {
|
|
173
|
+
return { input, output, source: "inferred" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeModalities(values: string[]): ModelModality[] {
|
|
180
|
+
const normalized = new Set<ModelModality>();
|
|
181
|
+
for (const value of values) {
|
|
182
|
+
const lower = value.toLowerCase();
|
|
183
|
+
if (lower === "text" || lower === "image" || lower === "audio" || lower === "embedding") {
|
|
184
|
+
normalized.add(lower);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Array.from(normalized);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function readJson(stream: NodeJS.ReadableStream): Promise<unknown> {
|
|
191
|
+
const chunks: Buffer[] = [];
|
|
192
|
+
for await (const chunk of stream) {
|
|
193
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
194
|
+
}
|
|
195
|
+
if (chunks.length === 0) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StoragePaths } from "../storage/files";
|
|
2
|
+
import { runCaptureRetention } from "../storage/captureRepository";
|
|
3
|
+
|
|
4
|
+
let retentionTimer: NodeJS.Timeout | null = null;
|
|
5
|
+
|
|
6
|
+
export function startCaptureRetentionWorker(paths: StoragePaths): void {
|
|
7
|
+
const run = async () => {
|
|
8
|
+
try {
|
|
9
|
+
await runCaptureRetention(paths);
|
|
10
|
+
} catch {
|
|
11
|
+
// ignore background errors
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
retentionTimer = setInterval(run, 10 * 60 * 1000);
|
|
16
|
+
retentionTimer.unref();
|
|
17
|
+
void run();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stopCaptureRetentionWorker(): void {
|
|
21
|
+
if (retentionTimer) {
|
|
22
|
+
clearInterval(retentionTimer);
|
|
23
|
+
retentionTimer = null;
|
|
24
|
+
}
|
|
25
|
+
}
|