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,396 @@
|
|
|
1
|
+
import { routeRequest } from "../routing/router";
|
|
2
|
+
import { selectPoolCandidates } from "../pools/scheduler";
|
|
3
|
+
import { pickBestProviderModelByCapabilities } from "../providers/modelRegistry";
|
|
4
|
+
import { getMediaEntry, getMediaPath } from "../storage/imageCache";
|
|
5
|
+
import { StoragePaths } from "../storage/files";
|
|
6
|
+
import { ImageGenerationRequest } from "../types";
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
export interface ImageGenerationRunResult {
|
|
11
|
+
model: string;
|
|
12
|
+
statusCode: number;
|
|
13
|
+
headers: Record<string, string | string[]>;
|
|
14
|
+
payload: unknown;
|
|
15
|
+
route: {
|
|
16
|
+
endpointId: string;
|
|
17
|
+
endpointName: string;
|
|
18
|
+
upstreamModel: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NormalizedGeneratedImage {
|
|
23
|
+
index: number;
|
|
24
|
+
url?: string;
|
|
25
|
+
b64_json?: string;
|
|
26
|
+
revised_prompt?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NormalizedImageGenerationResult {
|
|
30
|
+
model: string;
|
|
31
|
+
created: number;
|
|
32
|
+
images: NormalizedGeneratedImage[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function resolveGenerationModel(
|
|
36
|
+
paths: StoragePaths,
|
|
37
|
+
requestedModel?: string
|
|
38
|
+
): Promise<string | null> {
|
|
39
|
+
if (requestedModel) {
|
|
40
|
+
return requestedModel;
|
|
41
|
+
}
|
|
42
|
+
// Keep compatibility with existing /v1/images/generations fallback behavior.
|
|
43
|
+
return pickDefaultDiffusionModel(paths);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runImageGeneration(
|
|
47
|
+
paths: StoragePaths,
|
|
48
|
+
request: ImageGenerationRequest,
|
|
49
|
+
headers: Record<string, string | string[] | undefined>,
|
|
50
|
+
signal: AbortSignal
|
|
51
|
+
): Promise<ImageGenerationRunResult> {
|
|
52
|
+
const model = request.model
|
|
53
|
+
? request.model
|
|
54
|
+
: request.image_url
|
|
55
|
+
? await pickDefaultImageEditModel(paths)
|
|
56
|
+
: await resolveGenerationModel(paths, request.model);
|
|
57
|
+
if (!model) {
|
|
58
|
+
const error = new Error("No diffusion model available. Add or enable a provider model.") as Error & {
|
|
59
|
+
type: string;
|
|
60
|
+
retryable: boolean;
|
|
61
|
+
};
|
|
62
|
+
error.type = "no_diffusion_model";
|
|
63
|
+
error.retryable = false;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let body: { payload: unknown };
|
|
68
|
+
let outcome: Awaited<ReturnType<typeof routeRequest>>;
|
|
69
|
+
if (request.image_url) {
|
|
70
|
+
const chatPayload = {
|
|
71
|
+
model,
|
|
72
|
+
stream: false,
|
|
73
|
+
messages: [
|
|
74
|
+
{
|
|
75
|
+
role: "user",
|
|
76
|
+
content: [
|
|
77
|
+
{ type: "text", text: request.prompt },
|
|
78
|
+
{ type: "image_url", image_url: { url: request.image_url } },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
} as Record<string, unknown>;
|
|
83
|
+
try {
|
|
84
|
+
outcome = await routeRequest(
|
|
85
|
+
paths,
|
|
86
|
+
model,
|
|
87
|
+
"/v1/chat/completions",
|
|
88
|
+
chatPayload,
|
|
89
|
+
headers,
|
|
90
|
+
signal,
|
|
91
|
+
{
|
|
92
|
+
requiredInput: ["text", "image"],
|
|
93
|
+
requiredOutput: ["image"],
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const typed = error as Error & { type?: string };
|
|
98
|
+
if (typed.type !== "no_endpoints") {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
outcome = await routeRequest(
|
|
102
|
+
paths,
|
|
103
|
+
model,
|
|
104
|
+
"/v1/chat/completions",
|
|
105
|
+
chatPayload,
|
|
106
|
+
headers,
|
|
107
|
+
signal,
|
|
108
|
+
{
|
|
109
|
+
requiredInput: ["text"],
|
|
110
|
+
requiredOutput: ["image"],
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
body = await readBody(outcome.attempt.response);
|
|
115
|
+
body.payload = normalizeChatImagePayload(body.payload);
|
|
116
|
+
} else {
|
|
117
|
+
outcome = await routeRequest(
|
|
118
|
+
paths,
|
|
119
|
+
model,
|
|
120
|
+
"/v1/images/generations",
|
|
121
|
+
{ ...request, model } as Record<string, unknown>,
|
|
122
|
+
headers,
|
|
123
|
+
signal,
|
|
124
|
+
{
|
|
125
|
+
endpointType: "diffusion",
|
|
126
|
+
requiredInput: ["text"],
|
|
127
|
+
requiredOutput: ["image"],
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
body = await readBody(outcome.attempt.response);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
model,
|
|
135
|
+
statusCode: outcome.attempt.response.statusCode,
|
|
136
|
+
headers: outcome.attempt.response.headers,
|
|
137
|
+
payload: body.payload,
|
|
138
|
+
route: {
|
|
139
|
+
endpointId: outcome.attempt.endpoint.id,
|
|
140
|
+
endpointName: outcome.attempt.endpoint.name,
|
|
141
|
+
upstreamModel: outcome.attempt.upstreamModel,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function normalizeImageGenerationPayload(
|
|
147
|
+
paths: StoragePaths,
|
|
148
|
+
payload: unknown,
|
|
149
|
+
model: string
|
|
150
|
+
): Promise<NormalizedImageGenerationResult> {
|
|
151
|
+
const asObject = (payload ?? {}) as { created?: unknown; data?: unknown };
|
|
152
|
+
const created = typeof asObject.created === "number" ? asObject.created : Math.floor(Date.now() / 1000);
|
|
153
|
+
const data = Array.isArray(asObject.data) ? asObject.data : [];
|
|
154
|
+
const images: NormalizedGeneratedImage[] = [];
|
|
155
|
+
|
|
156
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
157
|
+
const item = (data[index] ?? {}) as {
|
|
158
|
+
url?: unknown;
|
|
159
|
+
b64_json?: unknown;
|
|
160
|
+
revised_prompt?: unknown;
|
|
161
|
+
};
|
|
162
|
+
const entry: NormalizedGeneratedImage = { index };
|
|
163
|
+
if (typeof item.revised_prompt === "string" && item.revised_prompt.length > 0) {
|
|
164
|
+
entry.revised_prompt = item.revised_prompt;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof item.b64_json === "string" && item.b64_json.length > 0) {
|
|
168
|
+
entry.b64_json = item.b64_json;
|
|
169
|
+
}
|
|
170
|
+
if (typeof item.url === "string" && item.url.length > 0) {
|
|
171
|
+
entry.url = item.url;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!entry.b64_json && entry.url) {
|
|
175
|
+
const extracted = await tryExtractBase64FromUrl(paths, entry.url);
|
|
176
|
+
if (extracted) {
|
|
177
|
+
entry.b64_json = extracted.b64;
|
|
178
|
+
if (!entry.url.startsWith("data:")) {
|
|
179
|
+
entry.url = `data:${extracted.mimeType};base64,${extracted.b64}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!entry.url && entry.b64_json) {
|
|
185
|
+
entry.url = `data:image/png;base64,${entry.b64_json}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
images.push(entry);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { model, created, images };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function normalizeChatImagePayload(payload: unknown): unknown {
|
|
195
|
+
const root = payload as {
|
|
196
|
+
created?: unknown;
|
|
197
|
+
choices?: unknown;
|
|
198
|
+
};
|
|
199
|
+
const created =
|
|
200
|
+
typeof root?.created === "number" ? root.created : Math.floor(Date.now() / 1000);
|
|
201
|
+
const choices = Array.isArray(root?.choices) ? root.choices : [];
|
|
202
|
+
const firstChoice = (choices[0] ?? null) as
|
|
203
|
+
| {
|
|
204
|
+
message?: { content?: unknown };
|
|
205
|
+
}
|
|
206
|
+
| null;
|
|
207
|
+
const content = firstChoice?.message?.content;
|
|
208
|
+
|
|
209
|
+
const data: Array<{ url?: string; b64_json?: string; revised_prompt?: string }> = [];
|
|
210
|
+
let revisedPrompt: string | undefined;
|
|
211
|
+
|
|
212
|
+
if (Array.isArray(content)) {
|
|
213
|
+
for (const item of content) {
|
|
214
|
+
if (!item || typeof item !== "object") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const typed = item as Record<string, unknown>;
|
|
218
|
+
const type = typeof typed.type === "string" ? typed.type : "";
|
|
219
|
+
if (!revisedPrompt && type === "text" && typeof typed.text === "string") {
|
|
220
|
+
revisedPrompt = typed.text.trim() || undefined;
|
|
221
|
+
}
|
|
222
|
+
if (type === "image_url") {
|
|
223
|
+
const imageUrlObject = typed.image_url as { url?: unknown } | undefined;
|
|
224
|
+
if (typeof imageUrlObject?.url === "string" && imageUrlObject.url.length > 0) {
|
|
225
|
+
data.push({ url: imageUrlObject.url });
|
|
226
|
+
}
|
|
227
|
+
} else if (type === "image" && typeof typed.image === "string" && typed.image.length > 0) {
|
|
228
|
+
data.push({ url: typed.image });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (typeof content === "string" && content.startsWith("data:image/")) {
|
|
234
|
+
data.push({ url: content });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (data.length === 0) {
|
|
238
|
+
const error = new Error("Upstream chat completion did not return any image output.") as Error & {
|
|
239
|
+
type: string;
|
|
240
|
+
retryable: boolean;
|
|
241
|
+
};
|
|
242
|
+
error.type = "invalid_upstream_response";
|
|
243
|
+
error.retryable = true;
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
if (revisedPrompt) {
|
|
247
|
+
data[0].revised_prompt = revisedPrompt;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { created, data };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function pickDefaultDiffusionModel(paths: StoragePaths): Promise<string | null> {
|
|
254
|
+
const smart = await selectPoolCandidates(
|
|
255
|
+
paths,
|
|
256
|
+
"smart",
|
|
257
|
+
{
|
|
258
|
+
requiredInput: ["text"],
|
|
259
|
+
requiredOutput: ["image"],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
operation: "images_generation",
|
|
263
|
+
stream: false,
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
if (smart && smart.candidates.length > 0) {
|
|
267
|
+
return "smart";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const byCapabilities = await pickBestProviderModelByCapabilities(
|
|
271
|
+
paths,
|
|
272
|
+
{ requiredInput: ["text"], requiredOutput: ["image"] },
|
|
273
|
+
"diffusion"
|
|
274
|
+
);
|
|
275
|
+
if (byCapabilities) {
|
|
276
|
+
return byCapabilities;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function pickDefaultImageEditModel(paths: StoragePaths): Promise<string | null> {
|
|
282
|
+
const smart = await selectPoolCandidates(
|
|
283
|
+
paths,
|
|
284
|
+
"smart",
|
|
285
|
+
{
|
|
286
|
+
requiredInput: ["image", "text"],
|
|
287
|
+
requiredOutput: ["image"],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
operation: "images_edits",
|
|
291
|
+
stream: false,
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
if (smart && smart.candidates.length > 0) {
|
|
295
|
+
return "smart";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const byCapabilities = await pickBestProviderModelByCapabilities(
|
|
299
|
+
paths,
|
|
300
|
+
{ requiredInput: ["image"], requiredOutput: ["image"] },
|
|
301
|
+
"diffusion"
|
|
302
|
+
);
|
|
303
|
+
if (byCapabilities) {
|
|
304
|
+
return byCapabilities;
|
|
305
|
+
}
|
|
306
|
+
return pickDefaultDiffusionModel(paths);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function readBody(response: {
|
|
310
|
+
body: NodeJS.ReadableStream;
|
|
311
|
+
headers: Record<string, string | string[]>;
|
|
312
|
+
}): Promise<{ payload: unknown }> {
|
|
313
|
+
const chunks: Buffer[] = [];
|
|
314
|
+
for await (const chunk of response.body) {
|
|
315
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
316
|
+
}
|
|
317
|
+
const buffer = Buffer.concat(chunks);
|
|
318
|
+
const contentType = normalizeContentType(response.headers);
|
|
319
|
+
if (contentType.includes("application/json")) {
|
|
320
|
+
try {
|
|
321
|
+
return { payload: JSON.parse(buffer.toString("utf8")) };
|
|
322
|
+
} catch {
|
|
323
|
+
return { payload: buffer };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { payload: buffer };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function normalizeContentType(headers: Record<string, string | string[]>): string {
|
|
330
|
+
const ct = headers["content-type"] ?? headers["Content-Type"];
|
|
331
|
+
if (Array.isArray(ct)) return ct.join(", ");
|
|
332
|
+
return ct ?? "";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function tryExtractBase64FromUrl(
|
|
336
|
+
paths: StoragePaths,
|
|
337
|
+
url: string
|
|
338
|
+
): Promise<{ b64: string; mimeType: string } | null> {
|
|
339
|
+
if (url.startsWith("data:")) {
|
|
340
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/i);
|
|
341
|
+
if (!match) return null;
|
|
342
|
+
return { mimeType: match[1], b64: match[2].replace(/\s+/g, "") };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const hash = extractLocalHash(url);
|
|
346
|
+
if (!hash) return null;
|
|
347
|
+
|
|
348
|
+
const mediaPath = await getMediaPath(paths, hash);
|
|
349
|
+
const mediaEntry = await getMediaEntry(paths, hash);
|
|
350
|
+
if (!mediaPath) return null;
|
|
351
|
+
const buffer = await fs.readFile(mediaPath);
|
|
352
|
+
const mimeType = mediaEntry?.mimeType ?? mimeFromExt(mediaPath);
|
|
353
|
+
return { mimeType, b64: buffer.toString("base64") };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function extractLocalHash(url: string): string | null {
|
|
357
|
+
const normalized = normalizeLocalUrl(url);
|
|
358
|
+
if (!normalized) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const mediaMatch = normalized.match(/^\/admin\/(media|images)\/([a-f0-9]{16})$/i);
|
|
362
|
+
if (!mediaMatch) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return mediaMatch[2];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizeLocalUrl(url: string): string | null {
|
|
369
|
+
if (url.startsWith("/")) {
|
|
370
|
+
return url;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const parsed = new URL(url);
|
|
374
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
if (!["localhost", "127.0.0.1", "::1"].includes(parsed.hostname)) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
return parsed.pathname;
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function mimeFromExt(filePath: string): string {
|
|
387
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
388
|
+
const map: Record<string, string> = {
|
|
389
|
+
png: "image/png",
|
|
390
|
+
jpg: "image/jpeg",
|
|
391
|
+
jpeg: "image/jpeg",
|
|
392
|
+
gif: "image/gif",
|
|
393
|
+
webp: "image/webp",
|
|
394
|
+
};
|
|
395
|
+
return map[ext] ?? "application/octet-stream";
|
|
396
|
+
}
|