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,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.routeRequest = routeRequest;
|
|
4
|
+
const httpClient_1 = require("../transport/httpClient");
|
|
5
|
+
const scheduler_1 = require("../pools/scheduler");
|
|
6
|
+
const registry_1 = require("../protocols/registry");
|
|
7
|
+
const modelRegistry_1 = require("../providers/modelRegistry");
|
|
8
|
+
const repository_1 = require("../providers/repository");
|
|
9
|
+
async function routeRequest(paths, publicModel, path, payload, headers, signal, requirements) {
|
|
10
|
+
const operation = (0, registry_1.routePathToOperation)(path);
|
|
11
|
+
const streamRequested = payload.stream === true;
|
|
12
|
+
const resolved = await (0, modelRegistry_1.resolveModel)(paths, publicModel, {
|
|
13
|
+
requiredInput: requirements?.requiredInput,
|
|
14
|
+
requiredOutput: requirements?.requiredOutput,
|
|
15
|
+
}, operation ? { operation, stream: streamRequested } : undefined);
|
|
16
|
+
if (resolved.kind === "ambiguous") {
|
|
17
|
+
const error = new Error(`Model '${resolved.input}' is ambiguous. Use canonical model ID: ${resolved.matches.join(", ")}`);
|
|
18
|
+
error.type = "invalid_request";
|
|
19
|
+
error.retryable = false;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
if (resolved.kind === "deprecated_pool_alias") {
|
|
23
|
+
const error = new Error(`Model alias '${resolved.input}' is deprecated. Use '${resolved.replacement}' instead.`);
|
|
24
|
+
error.type = "invalid_request";
|
|
25
|
+
error.retryable = false;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
if (resolved.kind === "none") {
|
|
29
|
+
const error = new Error(`Unknown model '${resolved.input}'`);
|
|
30
|
+
error.type = "no_endpoints";
|
|
31
|
+
error.retryable = false;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
if (resolved.kind === "pool") {
|
|
35
|
+
const poolSelection = await (0, scheduler_1.selectPoolCandidates)(paths, resolved.alias, {
|
|
36
|
+
requiredInput: requirements?.requiredInput,
|
|
37
|
+
requiredOutput: requirements?.requiredOutput,
|
|
38
|
+
}, operation ? { operation, stream: streamRequested } : undefined);
|
|
39
|
+
if (!poolSelection || poolSelection.candidates.length === 0) {
|
|
40
|
+
const exhaustedByLimits = poolSelection?.skipped.some((item) => item.reason === "cooldown" || item.reason === "request_budget_exhausted");
|
|
41
|
+
const streamUnsupported = poolSelection?.skipped.some((item) => item.reason === "stream_unsupported");
|
|
42
|
+
const error = new Error("No eligible endpoints for model");
|
|
43
|
+
error.type = exhaustedByLimits
|
|
44
|
+
? "rate_limited"
|
|
45
|
+
: streamUnsupported
|
|
46
|
+
? "protocol_stream_unsupported"
|
|
47
|
+
: "no_endpoints";
|
|
48
|
+
error.retryable = Boolean(exhaustedByLimits);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return routeWithPoolCandidates(paths, resolved.alias, path, payload, headers, signal, poolSelection.pool.id, poolSelection.candidates, requirements, operation, streamRequested);
|
|
52
|
+
}
|
|
53
|
+
if (resolved.candidates.length === 0) {
|
|
54
|
+
const error = new Error("No eligible endpoints for model");
|
|
55
|
+
error.type = "no_endpoints";
|
|
56
|
+
error.retryable = false;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return routeWithPoolCandidates(paths, resolved.canonicalId, path, payload, headers, signal, `model:${resolved.canonicalId}`, resolved.candidates, requirements, operation, streamRequested);
|
|
60
|
+
}
|
|
61
|
+
async function routeWithPoolCandidates(paths, publicModel, path, payload, headers, signal, poolId, candidates, requirements, operation, streamRequested) {
|
|
62
|
+
if (candidates.length === 0) {
|
|
63
|
+
const error = new Error("No eligible endpoints for model");
|
|
64
|
+
error.type = "no_endpoints";
|
|
65
|
+
error.retryable = false;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
let lastError = null;
|
|
69
|
+
let attempts = 0;
|
|
70
|
+
let rateLimitSwitches = 0;
|
|
71
|
+
const seenProviders = new Set();
|
|
72
|
+
const seenModels = new Set();
|
|
73
|
+
const triedModels = [];
|
|
74
|
+
const triedModelSet = new Set();
|
|
75
|
+
const isSmartAlias = publicModel === "smart";
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
attempts += 1;
|
|
78
|
+
seenProviders.add(candidate.providerId);
|
|
79
|
+
seenModels.add(candidate.modelId);
|
|
80
|
+
const candidateName = `${candidate.providerId}/${candidate.modelId}`;
|
|
81
|
+
if (!triedModelSet.has(candidateName)) {
|
|
82
|
+
triedModelSet.add(candidateName);
|
|
83
|
+
triedModels.push(candidateName);
|
|
84
|
+
}
|
|
85
|
+
const endpoint = (0, scheduler_1.buildEndpointFromCandidate)(candidate);
|
|
86
|
+
const adapter = (0, registry_1.getProtocolAdapter)(candidate.protocol);
|
|
87
|
+
if (!adapter) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const support = adapter.supports({
|
|
91
|
+
operation: operation ?? "chat_completions",
|
|
92
|
+
stream: streamRequested,
|
|
93
|
+
capabilities: candidate.capabilities,
|
|
94
|
+
requiredInput: requirements?.requiredInput,
|
|
95
|
+
requiredOutput: requirements?.requiredOutput,
|
|
96
|
+
});
|
|
97
|
+
if (!support.supported) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const timeoutMs = candidate.limits?.timeoutMs ?? 60_000;
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
await (0, scheduler_1.markPoolAttempt)(paths, candidate, (0, scheduler_1.estimateTokensFromPayload)(payload));
|
|
103
|
+
let requestData = null;
|
|
104
|
+
try {
|
|
105
|
+
requestData = await adapter.buildRequest({
|
|
106
|
+
paths,
|
|
107
|
+
operation: operation ?? "chat_completions",
|
|
108
|
+
stream: streamRequested,
|
|
109
|
+
path,
|
|
110
|
+
payload: { ...payload, model: candidate.upstreamModel },
|
|
111
|
+
publicModel,
|
|
112
|
+
upstreamModel: candidate.upstreamModel,
|
|
113
|
+
endpoint,
|
|
114
|
+
auth: candidate.auth,
|
|
115
|
+
config: candidate.protocolConfig,
|
|
116
|
+
});
|
|
117
|
+
const response = await (0, httpClient_1.proxyUpstream)(endpoint, requestData.path, requestData.payload, mergeForwardHeaders(headers, requestData.headers), timeoutMs, signal, { skipDefaultAuth: requestData.skipDefaultAuth });
|
|
118
|
+
const latency = Date.now() - start;
|
|
119
|
+
const classification = (0, httpClient_1.classifyHttpStatus)(response.statusCode);
|
|
120
|
+
if (isSmartAlias && shouldFailoverForIncompatibleStatus(response.statusCode)) {
|
|
121
|
+
await (0, scheduler_1.markPoolFailure)(paths, candidate, {
|
|
122
|
+
error: `operation_unsupported_${response.statusCode}`,
|
|
123
|
+
headers: response.headers,
|
|
124
|
+
});
|
|
125
|
+
await drainBody(response.body);
|
|
126
|
+
lastError = new Error(`Incompatible status ${response.statusCode}`);
|
|
127
|
+
lastError.type = classification.type;
|
|
128
|
+
lastError.retryable = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (classification.retryable) {
|
|
132
|
+
if (response.statusCode === 429) {
|
|
133
|
+
rateLimitSwitches += 1;
|
|
134
|
+
}
|
|
135
|
+
await (0, scheduler_1.markPoolFailure)(paths, candidate, {
|
|
136
|
+
error: classification.type,
|
|
137
|
+
rateLimited: response.statusCode === 429,
|
|
138
|
+
headers: response.headers,
|
|
139
|
+
});
|
|
140
|
+
await drainBody(response.body);
|
|
141
|
+
lastError = new Error(`Retryable status ${response.statusCode}`);
|
|
142
|
+
lastError.type = classification.type;
|
|
143
|
+
lastError.retryable = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const normalizedResponse = await maybeNormalizeResponse(adapter, {
|
|
147
|
+
operation: operation ?? "chat_completions",
|
|
148
|
+
stream: streamRequested,
|
|
149
|
+
path,
|
|
150
|
+
publicModel,
|
|
151
|
+
upstreamModel: candidate.upstreamModel,
|
|
152
|
+
requestPayload: requestData.payload,
|
|
153
|
+
upstreamResult: response,
|
|
154
|
+
config: candidate.protocolConfig,
|
|
155
|
+
}, response);
|
|
156
|
+
await (0, scheduler_1.markPoolSuccess)(paths, candidate, latency);
|
|
157
|
+
return {
|
|
158
|
+
attempt: {
|
|
159
|
+
endpoint,
|
|
160
|
+
upstreamModel: candidate.upstreamModel,
|
|
161
|
+
response: normalizedResponse,
|
|
162
|
+
pool: {
|
|
163
|
+
id: poolId,
|
|
164
|
+
alias: publicModel,
|
|
165
|
+
candidateAttempts: attempts,
|
|
166
|
+
failovers: Math.max(0, attempts - 1),
|
|
167
|
+
rateLimitSwitches,
|
|
168
|
+
distinctProviders: seenProviders.size,
|
|
169
|
+
distinctModels: seenModels.size,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
retryable: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
let classified = (0, httpClient_1.classifyUpstreamError)(error);
|
|
177
|
+
if (classified.type === "tls_verify_failed" &&
|
|
178
|
+
endpoint.insecureTls !== true &&
|
|
179
|
+
requestData &&
|
|
180
|
+
isHostnameAllowlisted(candidate)) {
|
|
181
|
+
const insecureEndpoint = { ...endpoint, insecureTls: true };
|
|
182
|
+
await (0, scheduler_1.markPoolAttempt)(paths, candidate, (0, scheduler_1.estimateTokensFromPayload)(payload));
|
|
183
|
+
try {
|
|
184
|
+
const retryResponse = await (0, httpClient_1.proxyUpstream)(insecureEndpoint, requestData.path, requestData.payload, mergeForwardHeaders(headers, requestData.headers), timeoutMs, signal, { skipDefaultAuth: requestData.skipDefaultAuth });
|
|
185
|
+
const latency = Date.now() - start;
|
|
186
|
+
const retryClassification = (0, httpClient_1.classifyHttpStatus)(retryResponse.statusCode);
|
|
187
|
+
if (isSmartAlias && shouldFailoverForIncompatibleStatus(retryResponse.statusCode)) {
|
|
188
|
+
await (0, scheduler_1.markPoolFailure)(paths, candidate, {
|
|
189
|
+
error: `operation_unsupported_${retryResponse.statusCode}`,
|
|
190
|
+
headers: retryResponse.headers,
|
|
191
|
+
});
|
|
192
|
+
await drainBody(retryResponse.body);
|
|
193
|
+
lastError = new Error(`Incompatible status ${retryResponse.statusCode}`);
|
|
194
|
+
lastError.type = retryClassification.type;
|
|
195
|
+
lastError.retryable = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (retryClassification.retryable) {
|
|
199
|
+
if (retryResponse.statusCode === 429) {
|
|
200
|
+
rateLimitSwitches += 1;
|
|
201
|
+
}
|
|
202
|
+
await (0, scheduler_1.markPoolFailure)(paths, candidate, {
|
|
203
|
+
error: retryClassification.type,
|
|
204
|
+
rateLimited: retryResponse.statusCode === 429,
|
|
205
|
+
headers: retryResponse.headers,
|
|
206
|
+
});
|
|
207
|
+
await drainBody(retryResponse.body);
|
|
208
|
+
lastError = new Error(`Retryable status ${retryResponse.statusCode}`);
|
|
209
|
+
lastError.type = retryClassification.type;
|
|
210
|
+
lastError.retryable = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const normalizedRetryResponse = await maybeNormalizeResponse(adapter, {
|
|
214
|
+
operation: operation ?? "chat_completions",
|
|
215
|
+
stream: streamRequested,
|
|
216
|
+
path,
|
|
217
|
+
publicModel,
|
|
218
|
+
upstreamModel: candidate.upstreamModel,
|
|
219
|
+
requestPayload: requestData.payload,
|
|
220
|
+
upstreamResult: retryResponse,
|
|
221
|
+
config: candidate.protocolConfig,
|
|
222
|
+
}, retryResponse);
|
|
223
|
+
if (retryClassification.type === "ok") {
|
|
224
|
+
await (0, repository_1.setProviderModelInsecureTls)(paths, candidate.providerId, candidate.providerModelId ?? candidate.modelId, true);
|
|
225
|
+
}
|
|
226
|
+
await (0, scheduler_1.markPoolSuccess)(paths, candidate, latency);
|
|
227
|
+
return {
|
|
228
|
+
attempt: {
|
|
229
|
+
endpoint: insecureEndpoint,
|
|
230
|
+
upstreamModel: candidate.upstreamModel,
|
|
231
|
+
response: normalizedRetryResponse,
|
|
232
|
+
pool: {
|
|
233
|
+
id: poolId,
|
|
234
|
+
alias: publicModel,
|
|
235
|
+
candidateAttempts: attempts,
|
|
236
|
+
failovers: Math.max(0, attempts - 1),
|
|
237
|
+
rateLimitSwitches,
|
|
238
|
+
distinctProviders: seenProviders.size,
|
|
239
|
+
distinctModels: seenModels.size,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
retryable: false,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (retryError) {
|
|
246
|
+
classified = (0, httpClient_1.classifyUpstreamError)(retryError);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (classified.type === "tls_verify_failed") {
|
|
250
|
+
classified.message = tlsVerifyFailureMessage(candidate);
|
|
251
|
+
}
|
|
252
|
+
lastError = classified;
|
|
253
|
+
await (0, scheduler_1.markPoolFailure)(paths, candidate, {
|
|
254
|
+
error: classified.type,
|
|
255
|
+
rateLimited: classified.type === "rate_limited",
|
|
256
|
+
});
|
|
257
|
+
if (!classified.retryable) {
|
|
258
|
+
throw maybeEnrichSmartError(classified, triedModels, poolId, isSmartAlias);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (lastError) {
|
|
263
|
+
throw maybeEnrichSmartError(lastError, triedModels, poolId, isSmartAlias);
|
|
264
|
+
}
|
|
265
|
+
const error = new Error("No endpoints succeeded");
|
|
266
|
+
error.type = "no_endpoints";
|
|
267
|
+
error.retryable = true;
|
|
268
|
+
throw maybeEnrichSmartError(error, triedModels, poolId, isSmartAlias);
|
|
269
|
+
}
|
|
270
|
+
function shouldFailoverForIncompatibleStatus(statusCode) {
|
|
271
|
+
return statusCode === 404 || statusCode === 405 || statusCode === 501;
|
|
272
|
+
}
|
|
273
|
+
function maybeEnrichSmartError(error, triedModels, poolId, shouldEnrich) {
|
|
274
|
+
if (!shouldEnrich) {
|
|
275
|
+
return error;
|
|
276
|
+
}
|
|
277
|
+
error.poolId = poolId;
|
|
278
|
+
error.triedModels = triedModels.slice(0, 10);
|
|
279
|
+
if (error.triedModels.length > 0) {
|
|
280
|
+
error.message = `Smart routing failed. Tried models: ${error.triedModels.join(", ")}. Cause: ${error.message}`;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
error.message = `Smart routing failed. No eligible models. Cause: ${error.message}`;
|
|
284
|
+
}
|
|
285
|
+
return error;
|
|
286
|
+
}
|
|
287
|
+
function isHostnameAllowlisted(candidate) {
|
|
288
|
+
const allowlist = candidate.autoInsecureTlsDomains ?? [];
|
|
289
|
+
if (allowlist.length === 0) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const hostname = getHostname(candidate.baseUrl);
|
|
293
|
+
if (!hostname) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return allowlist.some((suffix) => matchesDomainSuffix(hostname, suffix));
|
|
297
|
+
}
|
|
298
|
+
function getHostname(baseUrl) {
|
|
299
|
+
try {
|
|
300
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function matchesDomainSuffix(hostname, suffix) {
|
|
307
|
+
const normalizedSuffix = suffix.trim().toLowerCase();
|
|
308
|
+
if (!normalizedSuffix) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
return hostname === normalizedSuffix || hostname.endsWith(`.${normalizedSuffix}`);
|
|
312
|
+
}
|
|
313
|
+
function tlsVerifyFailureMessage(candidate) {
|
|
314
|
+
return `Upstream TLS verify failed for ${candidate.providerId}/${candidate.modelId}. Configure provider/model insecureTls or provider allowlist.`;
|
|
315
|
+
}
|
|
316
|
+
async function maybeNormalizeResponse(adapter, context, fallback) {
|
|
317
|
+
if (!adapter.normalizeResponse) {
|
|
318
|
+
return fallback;
|
|
319
|
+
}
|
|
320
|
+
return adapter.normalizeResponse(context);
|
|
321
|
+
}
|
|
322
|
+
async function drainBody(stream) {
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
stream.on("end", resolve);
|
|
325
|
+
stream.on("close", resolve);
|
|
326
|
+
stream.on("error", resolve);
|
|
327
|
+
stream.resume();
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function mergeForwardHeaders(base, extras) {
|
|
331
|
+
if (!extras || Object.keys(extras).length === 0) {
|
|
332
|
+
return base;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
...base,
|
|
336
|
+
...extras,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
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.resolveGenerationModel = resolveGenerationModel;
|
|
7
|
+
exports.runImageGeneration = runImageGeneration;
|
|
8
|
+
exports.normalizeImageGenerationPayload = normalizeImageGenerationPayload;
|
|
9
|
+
exports.normalizeChatImagePayload = normalizeChatImagePayload;
|
|
10
|
+
const router_1 = require("../routing/router");
|
|
11
|
+
const scheduler_1 = require("../pools/scheduler");
|
|
12
|
+
const modelRegistry_1 = require("../providers/modelRegistry");
|
|
13
|
+
const imageCache_1 = require("../storage/imageCache");
|
|
14
|
+
const fs_1 = require("fs");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
async function resolveGenerationModel(paths, requestedModel) {
|
|
17
|
+
if (requestedModel) {
|
|
18
|
+
return requestedModel;
|
|
19
|
+
}
|
|
20
|
+
// Keep compatibility with existing /v1/images/generations fallback behavior.
|
|
21
|
+
return pickDefaultDiffusionModel(paths);
|
|
22
|
+
}
|
|
23
|
+
async function runImageGeneration(paths, request, headers, signal) {
|
|
24
|
+
const model = request.model
|
|
25
|
+
? request.model
|
|
26
|
+
: request.image_url
|
|
27
|
+
? await pickDefaultImageEditModel(paths)
|
|
28
|
+
: await resolveGenerationModel(paths, request.model);
|
|
29
|
+
if (!model) {
|
|
30
|
+
const error = new Error("No diffusion model available. Add or enable a provider model.");
|
|
31
|
+
error.type = "no_diffusion_model";
|
|
32
|
+
error.retryable = false;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
let body;
|
|
36
|
+
let outcome;
|
|
37
|
+
if (request.image_url) {
|
|
38
|
+
const chatPayload = {
|
|
39
|
+
model,
|
|
40
|
+
stream: false,
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: "user",
|
|
44
|
+
content: [
|
|
45
|
+
{ type: "text", text: request.prompt },
|
|
46
|
+
{ type: "image_url", image_url: { url: request.image_url } },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
outcome = await (0, router_1.routeRequest)(paths, model, "/v1/chat/completions", chatPayload, headers, signal, {
|
|
53
|
+
requiredInput: ["text", "image"],
|
|
54
|
+
requiredOutput: ["image"],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const typed = error;
|
|
59
|
+
if (typed.type !== "no_endpoints") {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
outcome = await (0, router_1.routeRequest)(paths, model, "/v1/chat/completions", chatPayload, headers, signal, {
|
|
63
|
+
requiredInput: ["text"],
|
|
64
|
+
requiredOutput: ["image"],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
body = await readBody(outcome.attempt.response);
|
|
68
|
+
body.payload = normalizeChatImagePayload(body.payload);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
outcome = await (0, router_1.routeRequest)(paths, model, "/v1/images/generations", { ...request, model }, headers, signal, {
|
|
72
|
+
endpointType: "diffusion",
|
|
73
|
+
requiredInput: ["text"],
|
|
74
|
+
requiredOutput: ["image"],
|
|
75
|
+
});
|
|
76
|
+
body = await readBody(outcome.attempt.response);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
model,
|
|
80
|
+
statusCode: outcome.attempt.response.statusCode,
|
|
81
|
+
headers: outcome.attempt.response.headers,
|
|
82
|
+
payload: body.payload,
|
|
83
|
+
route: {
|
|
84
|
+
endpointId: outcome.attempt.endpoint.id,
|
|
85
|
+
endpointName: outcome.attempt.endpoint.name,
|
|
86
|
+
upstreamModel: outcome.attempt.upstreamModel,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function normalizeImageGenerationPayload(paths, payload, model) {
|
|
91
|
+
const asObject = (payload ?? {});
|
|
92
|
+
const created = typeof asObject.created === "number" ? asObject.created : Math.floor(Date.now() / 1000);
|
|
93
|
+
const data = Array.isArray(asObject.data) ? asObject.data : [];
|
|
94
|
+
const images = [];
|
|
95
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
96
|
+
const item = (data[index] ?? {});
|
|
97
|
+
const entry = { index };
|
|
98
|
+
if (typeof item.revised_prompt === "string" && item.revised_prompt.length > 0) {
|
|
99
|
+
entry.revised_prompt = item.revised_prompt;
|
|
100
|
+
}
|
|
101
|
+
if (typeof item.b64_json === "string" && item.b64_json.length > 0) {
|
|
102
|
+
entry.b64_json = item.b64_json;
|
|
103
|
+
}
|
|
104
|
+
if (typeof item.url === "string" && item.url.length > 0) {
|
|
105
|
+
entry.url = item.url;
|
|
106
|
+
}
|
|
107
|
+
if (!entry.b64_json && entry.url) {
|
|
108
|
+
const extracted = await tryExtractBase64FromUrl(paths, entry.url);
|
|
109
|
+
if (extracted) {
|
|
110
|
+
entry.b64_json = extracted.b64;
|
|
111
|
+
if (!entry.url.startsWith("data:")) {
|
|
112
|
+
entry.url = `data:${extracted.mimeType};base64,${extracted.b64}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!entry.url && entry.b64_json) {
|
|
117
|
+
entry.url = `data:image/png;base64,${entry.b64_json}`;
|
|
118
|
+
}
|
|
119
|
+
images.push(entry);
|
|
120
|
+
}
|
|
121
|
+
return { model, created, images };
|
|
122
|
+
}
|
|
123
|
+
function normalizeChatImagePayload(payload) {
|
|
124
|
+
const root = payload;
|
|
125
|
+
const created = typeof root?.created === "number" ? root.created : Math.floor(Date.now() / 1000);
|
|
126
|
+
const choices = Array.isArray(root?.choices) ? root.choices : [];
|
|
127
|
+
const firstChoice = (choices[0] ?? null);
|
|
128
|
+
const content = firstChoice?.message?.content;
|
|
129
|
+
const data = [];
|
|
130
|
+
let revisedPrompt;
|
|
131
|
+
if (Array.isArray(content)) {
|
|
132
|
+
for (const item of content) {
|
|
133
|
+
if (!item || typeof item !== "object") {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const typed = item;
|
|
137
|
+
const type = typeof typed.type === "string" ? typed.type : "";
|
|
138
|
+
if (!revisedPrompt && type === "text" && typeof typed.text === "string") {
|
|
139
|
+
revisedPrompt = typed.text.trim() || undefined;
|
|
140
|
+
}
|
|
141
|
+
if (type === "image_url") {
|
|
142
|
+
const imageUrlObject = typed.image_url;
|
|
143
|
+
if (typeof imageUrlObject?.url === "string" && imageUrlObject.url.length > 0) {
|
|
144
|
+
data.push({ url: imageUrlObject.url });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (type === "image" && typeof typed.image === "string" && typed.image.length > 0) {
|
|
148
|
+
data.push({ url: typed.image });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (typeof content === "string" && content.startsWith("data:image/")) {
|
|
153
|
+
data.push({ url: content });
|
|
154
|
+
}
|
|
155
|
+
if (data.length === 0) {
|
|
156
|
+
const error = new Error("Upstream chat completion did not return any image output.");
|
|
157
|
+
error.type = "invalid_upstream_response";
|
|
158
|
+
error.retryable = true;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (revisedPrompt) {
|
|
162
|
+
data[0].revised_prompt = revisedPrompt;
|
|
163
|
+
}
|
|
164
|
+
return { created, data };
|
|
165
|
+
}
|
|
166
|
+
async function pickDefaultDiffusionModel(paths) {
|
|
167
|
+
const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
|
|
168
|
+
requiredInput: ["text"],
|
|
169
|
+
requiredOutput: ["image"],
|
|
170
|
+
}, {
|
|
171
|
+
operation: "images_generation",
|
|
172
|
+
stream: false,
|
|
173
|
+
});
|
|
174
|
+
if (smart && smart.candidates.length > 0) {
|
|
175
|
+
return "smart";
|
|
176
|
+
}
|
|
177
|
+
const byCapabilities = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["text"], requiredOutput: ["image"] }, "diffusion");
|
|
178
|
+
if (byCapabilities) {
|
|
179
|
+
return byCapabilities;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
async function pickDefaultImageEditModel(paths) {
|
|
184
|
+
const smart = await (0, scheduler_1.selectPoolCandidates)(paths, "smart", {
|
|
185
|
+
requiredInput: ["image", "text"],
|
|
186
|
+
requiredOutput: ["image"],
|
|
187
|
+
}, {
|
|
188
|
+
operation: "images_edits",
|
|
189
|
+
stream: false,
|
|
190
|
+
});
|
|
191
|
+
if (smart && smart.candidates.length > 0) {
|
|
192
|
+
return "smart";
|
|
193
|
+
}
|
|
194
|
+
const byCapabilities = await (0, modelRegistry_1.pickBestProviderModelByCapabilities)(paths, { requiredInput: ["image"], requiredOutput: ["image"] }, "diffusion");
|
|
195
|
+
if (byCapabilities) {
|
|
196
|
+
return byCapabilities;
|
|
197
|
+
}
|
|
198
|
+
return pickDefaultDiffusionModel(paths);
|
|
199
|
+
}
|
|
200
|
+
async function readBody(response) {
|
|
201
|
+
const chunks = [];
|
|
202
|
+
for await (const chunk of response.body) {
|
|
203
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
204
|
+
}
|
|
205
|
+
const buffer = Buffer.concat(chunks);
|
|
206
|
+
const contentType = normalizeContentType(response.headers);
|
|
207
|
+
if (contentType.includes("application/json")) {
|
|
208
|
+
try {
|
|
209
|
+
return { payload: JSON.parse(buffer.toString("utf8")) };
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return { payload: buffer };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { payload: buffer };
|
|
216
|
+
}
|
|
217
|
+
function normalizeContentType(headers) {
|
|
218
|
+
const ct = headers["content-type"] ?? headers["Content-Type"];
|
|
219
|
+
if (Array.isArray(ct))
|
|
220
|
+
return ct.join(", ");
|
|
221
|
+
return ct ?? "";
|
|
222
|
+
}
|
|
223
|
+
async function tryExtractBase64FromUrl(paths, url) {
|
|
224
|
+
if (url.startsWith("data:")) {
|
|
225
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/i);
|
|
226
|
+
if (!match)
|
|
227
|
+
return null;
|
|
228
|
+
return { mimeType: match[1], b64: match[2].replace(/\s+/g, "") };
|
|
229
|
+
}
|
|
230
|
+
const hash = extractLocalHash(url);
|
|
231
|
+
if (!hash)
|
|
232
|
+
return null;
|
|
233
|
+
const mediaPath = await (0, imageCache_1.getMediaPath)(paths, hash);
|
|
234
|
+
const mediaEntry = await (0, imageCache_1.getMediaEntry)(paths, hash);
|
|
235
|
+
if (!mediaPath)
|
|
236
|
+
return null;
|
|
237
|
+
const buffer = await fs_1.promises.readFile(mediaPath);
|
|
238
|
+
const mimeType = mediaEntry?.mimeType ?? mimeFromExt(mediaPath);
|
|
239
|
+
return { mimeType, b64: buffer.toString("base64") };
|
|
240
|
+
}
|
|
241
|
+
function extractLocalHash(url) {
|
|
242
|
+
const normalized = normalizeLocalUrl(url);
|
|
243
|
+
if (!normalized) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const mediaMatch = normalized.match(/^\/admin\/(media|images)\/([a-f0-9]{16})$/i);
|
|
247
|
+
if (!mediaMatch) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return mediaMatch[2];
|
|
251
|
+
}
|
|
252
|
+
function normalizeLocalUrl(url) {
|
|
253
|
+
if (url.startsWith("/")) {
|
|
254
|
+
return url;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const parsed = new URL(url);
|
|
258
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (!["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return parsed.pathname;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function mimeFromExt(filePath) {
|
|
271
|
+
const ext = path_1.default.extname(filePath).slice(1).toLowerCase();
|
|
272
|
+
const map = {
|
|
273
|
+
png: "image/png",
|
|
274
|
+
jpg: "image/jpeg",
|
|
275
|
+
jpeg: "image/jpeg",
|
|
276
|
+
gif: "image/gif",
|
|
277
|
+
webp: "image/webp",
|
|
278
|
+
};
|
|
279
|
+
return map[ext] ?? "application/octet-stream";
|
|
280
|
+
}
|