imageforge-mcp 0.4.0 → 0.6.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/README.md +216 -54
- package/dist/externalProviders.d.ts +71 -0
- package/dist/externalProviders.js +891 -0
- package/dist/externalProviders.js.map +1 -0
- package/dist/index.js +63 -12
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +41 -2
- package/dist/providers.js +231 -3
- package/dist/providers.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { loadRemoteImage } from "./imageInput.js";
|
|
4
|
+
export const EXTERNAL_PROVIDER_IDS = [
|
|
5
|
+
"google",
|
|
6
|
+
"xai",
|
|
7
|
+
"stability",
|
|
8
|
+
"ideogram",
|
|
9
|
+
"recraft",
|
|
10
|
+
"tencent",
|
|
11
|
+
"baidu",
|
|
12
|
+
"playground",
|
|
13
|
+
"bfl",
|
|
14
|
+
"luma",
|
|
15
|
+
"krea",
|
|
16
|
+
"runway",
|
|
17
|
+
"leonardo",
|
|
18
|
+
"bria",
|
|
19
|
+
"freepik",
|
|
20
|
+
"alibaba",
|
|
21
|
+
"volcengine",
|
|
22
|
+
"kling",
|
|
23
|
+
"hidream",
|
|
24
|
+
];
|
|
25
|
+
const definitions = [
|
|
26
|
+
{
|
|
27
|
+
id: "google", name: "Google Gemini", docsUrl: "https://ai.google.dev/gemini-api/docs/image-generation",
|
|
28
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta", baseUrlEnv: "GOOGLE_BASE_URL",
|
|
29
|
+
apiKeyEnv: "GOOGLE_API_KEY", modelEnv: "GOOGLE_IMAGE_MODEL", defaultModel: "gemini-3.1-flash-image",
|
|
30
|
+
models: ["gemini-3.1-flash-image", "gemini-3.1-flash-lite-image"],
|
|
31
|
+
referenceModes: ["auto", "content", "style"], maxReferences: 14, async: false,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "xai", name: "xAI", docsUrl: "https://docs.x.ai/developers/model-capabilities/images/generation",
|
|
35
|
+
baseUrl: "https://api.x.ai/v1", baseUrlEnv: "XAI_BASE_URL", apiKeyEnv: "XAI_API_KEY",
|
|
36
|
+
modelEnv: "XAI_IMAGE_MODEL", defaultModel: "grok-imagine-image-2.0", models: ["grok-imagine-image-2.0"],
|
|
37
|
+
referenceModes: ["auto", "content"], maxReferences: 1, async: false,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "stability", name: "Stability AI", docsUrl: "https://platform.stability.ai/docs/api-reference",
|
|
41
|
+
baseUrl: "https://api.stability.ai", baseUrlEnv: "STABILITY_BASE_URL", apiKeyEnv: "STABILITY_API_KEY",
|
|
42
|
+
modelEnv: "STABILITY_IMAGE_MODEL", defaultModel: "stable-image-ultra",
|
|
43
|
+
models: ["stable-image-ultra", "stable-image-core", "sd3.5-large", "sd3.5-large-turbo", "sd3.5-medium", "sd3.5-flash"],
|
|
44
|
+
referenceModes: ["auto", "content"], maxReferences: 1, async: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "ideogram", name: "Ideogram", docsUrl: "https://developer.ideogram.ai/api-reference/api-reference/generate-v4",
|
|
48
|
+
baseUrl: "https://api.ideogram.ai", baseUrlEnv: "IDEOGRAM_BASE_URL", apiKeyEnv: "IDEOGRAM_API_KEY",
|
|
49
|
+
modelEnv: "IDEOGRAM_IMAGE_MODEL", defaultModel: "ideogram-v4", models: ["ideogram-v4", "p-image-ideogram", "ideogram-v3"],
|
|
50
|
+
referenceModes: ["character", "style"], maxReferences: 5, async: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "recraft", name: "Recraft", docsUrl: "https://www.recraft.ai/docs/api-reference/endpoints",
|
|
54
|
+
baseUrl: "https://external.api.recraft.ai/v1", baseUrlEnv: "RECRAFT_BASE_URL", apiKeyEnv: "RECRAFT_API_KEY",
|
|
55
|
+
modelEnv: "RECRAFT_IMAGE_MODEL", defaultModel: "recraftv4_1",
|
|
56
|
+
models: ["recraftv4_1", "recraftv4_1_pro", "recraftv4_1_utility", "recraftv4_1_utility_pro", "recraftv4", "recraftv4_pro", "recraftv4_styles", "recraftv4_styles_pro", "recraftv3", "recraftv2"],
|
|
57
|
+
referenceModes: ["style"], maxReferences: 5, async: false,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "tencent", name: "Tencent Hunyuan", docsUrl: "https://cloud.tencent.com/document/product/1823/135745",
|
|
61
|
+
baseUrl: "https://tokenhub.tencentmaas.com", baseUrlEnv: "TENCENT_BASE_URL", apiKeyEnv: "TENCENT_API_KEY",
|
|
62
|
+
modelEnv: "TENCENT_IMAGE_MODEL", defaultModel: "hy-image-v3", models: ["hy-image-v3"],
|
|
63
|
+
referenceModes: ["auto", "content", "character"], maxReferences: 3, async: false,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "baidu", name: "Baidu Qianfan", docsUrl: "https://cloud.baidu.com/doc/qianfan-api/s/8m7u6un8a",
|
|
67
|
+
baseUrl: "https://qianfan.baidubce.com", baseUrlEnv: "BAIDU_BASE_URL", apiKeyEnv: "BAIDU_API_KEY",
|
|
68
|
+
modelEnv: "BAIDU_IMAGE_MODEL", defaultModel: "ernie-image", models: ["ernie-image"],
|
|
69
|
+
referenceModes: ["auto", "content"], maxReferences: 1, async: false,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "playground", name: "Playground AI", docsUrl: "https://docs.playground.com/reference/image-generation",
|
|
73
|
+
baseUrl: "https://playground.com", baseUrlEnv: "PLAYGROUND_BASE_URL", apiKeyEnv: "PLAYGROUND_API_KEY",
|
|
74
|
+
modelEnv: "PLAYGROUND_IMAGE_MODEL", defaultModel: "Playground_v3",
|
|
75
|
+
models: ["Playground_v3", "Playground_v2.5", "Photorealism"],
|
|
76
|
+
referenceModes: ["auto", "content"], maxReferences: 1, async: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "bfl", name: "Black Forest Labs", docsUrl: "https://docs.bfl.ai/flux_2/flux2_overview",
|
|
80
|
+
baseUrl: "https://api.bfl.ai", baseUrlEnv: "BFL_BASE_URL", apiKeyEnv: "BFL_API_KEY",
|
|
81
|
+
modelEnv: "BFL_IMAGE_MODEL", defaultModel: "flux-2-pro",
|
|
82
|
+
models: ["flux-2-max", "flux-2-pro", "flux-2-flex", "flux-2-klein-4b", "flux-2-klein-9b"],
|
|
83
|
+
referenceModes: ["auto", "content", "style", "character"], maxReferences: 8, async: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "luma", name: "Luma", docsUrl: "https://docs.agents.lumalabs.ai/guides/images/generation/",
|
|
87
|
+
baseUrl: "https://agents.lumalabs.ai/v1", baseUrlEnv: "LUMA_BASE_URL", apiKeyEnv: "LUMA_AGENTS_API_KEY",
|
|
88
|
+
modelEnv: "LUMA_IMAGE_MODEL", defaultModel: "uni-1", models: ["uni-1", "uni-1-max"],
|
|
89
|
+
referenceModes: ["auto", "content", "style"], maxReferences: 9, async: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "krea", name: "Krea", docsUrl: "https://www.krea.ai/docs/developers/krea-2/overview",
|
|
93
|
+
baseUrl: "https://api.krea.ai", baseUrlEnv: "KREA_BASE_URL", apiKeyEnv: "KREA_API_KEY",
|
|
94
|
+
modelEnv: "KREA_IMAGE_MODEL", defaultModel: "krea-2/medium",
|
|
95
|
+
models: ["krea-2/medium", "krea-2/large", "krea-2/medium-turbo"],
|
|
96
|
+
referenceModes: ["style"], maxReferences: 4, async: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "runway", name: "Runway", docsUrl: "https://docs.dev.runwayml.com/api/",
|
|
100
|
+
baseUrl: "https://api.dev.runwayml.com", baseUrlEnv: "RUNWAY_BASE_URL", apiKeyEnv: "RUNWAY_API_KEY",
|
|
101
|
+
modelEnv: "RUNWAY_IMAGE_MODEL", defaultModel: "gen4_image", models: ["gen4_image", "gen4_image_turbo"],
|
|
102
|
+
referenceModes: ["auto", "content", "style"], maxReferences: 3, async: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "leonardo", name: "Leonardo", docsUrl: "https://docs.leonardo.ai/reference/creategeneration-1",
|
|
106
|
+
baseUrl: "https://cloud.leonardo.ai/api/rest/v2", baseUrlEnv: "LEONARDO_BASE_URL", apiKeyEnv: "LEONARDO_API_KEY",
|
|
107
|
+
modelEnv: "LEONARDO_IMAGE_MODEL", defaultModel: "phoenix-v1.0",
|
|
108
|
+
models: ["phoenix-v1.0", "phoenix-v0.9", "lucid-origin", "lucid-realism"],
|
|
109
|
+
referenceModes: ["auto", "content", "style", "character"], maxReferences: 4, async: true,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "bria", name: "Bria", docsUrl: "https://docs.bria.ai/image-generation/v2-endpoints/image-generate",
|
|
113
|
+
baseUrl: "https://engine.prod.bria-api.com/v2", baseUrlEnv: "BRIA_BASE_URL", apiKeyEnv: "BRIA_API_KEY",
|
|
114
|
+
modelEnv: "BRIA_IMAGE_MODEL", defaultModel: "fibo", models: ["fibo", "fibo-lite"],
|
|
115
|
+
referenceModes: ["auto", "content", "style"], maxReferences: 1, async: true,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "freepik", name: "Freepik Mystic", docsUrl: "https://docs.freepik.com/api-reference/mystic/post-mystic",
|
|
119
|
+
baseUrl: "https://api.magnific.com", baseUrlEnv: "FREEPIK_BASE_URL", apiKeyEnv: "FREEPIK_API_KEY",
|
|
120
|
+
modelEnv: "FREEPIK_IMAGE_MODEL", defaultModel: "realism",
|
|
121
|
+
models: ["realism", "fluid", "zen", "flexible", "super_real", "editorial_portraits"],
|
|
122
|
+
referenceModes: ["content", "style"], maxReferences: 1, async: true,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "alibaba", name: "Alibaba Model Studio", docsUrl: "https://www.alibabacloud.com/help/en/model-studio/qwen-image-api",
|
|
126
|
+
baseUrl: "https://dashscope-intl.aliyuncs.com", baseUrlEnv: "ALIBABA_BASE_URL", apiKeyEnv: "ALIBABA_API_KEY",
|
|
127
|
+
modelEnv: "ALIBABA_IMAGE_MODEL", defaultModel: "qwen-image-3.0-pro",
|
|
128
|
+
models: ["qwen-image-3.0-pro", "qwen-image-3.0", "wan2.7-image-pro", "wan2.7-image"],
|
|
129
|
+
referenceModes: ["auto", "content", "style", "character"], maxReferences: 9, async: true,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "volcengine", name: "Volcengine Ark", docsUrl: "https://docs.volcengine.com/docs/82379/1541523",
|
|
133
|
+
baseUrl: "https://ark.cn-beijing.volces.com/api/v3", baseUrlEnv: "VOLCENGINE_BASE_URL", apiKeyEnv: "VOLCENGINE_API_KEY",
|
|
134
|
+
modelEnv: "VOLCENGINE_IMAGE_MODEL", defaultModel: "doubao-seedream-5-0-pro-260628",
|
|
135
|
+
models: ["doubao-seedream-5-0-pro-260628", "doubao-seedream-4-5-250815", "doubao-seedream-4-0-250828"],
|
|
136
|
+
referenceModes: ["auto", "content", "style", "character"], maxReferences: 10, async: false,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "kling", name: "Kling AI", docsUrl: "https://app.klingai.com/global/dev/document-api/quickStart/userManual",
|
|
140
|
+
baseUrl: "https://api.klingai.com", baseUrlEnv: "KLING_BASE_URL", apiKeyEnv: "KLING_ACCESS_KEY",
|
|
141
|
+
modelEnv: "KLING_IMAGE_MODEL", defaultModel: "kling-v3", models: ["kling-v3", "kling-v2-1"],
|
|
142
|
+
referenceModes: ["auto", "content", "style", "character"], maxReferences: 4, async: true,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "hidream", name: "HiDream", docsUrl: "https://hidreamai.com/doc/txt2img/request",
|
|
146
|
+
baseUrl: "https://www.hidreamai.com", baseUrlEnv: "HIDREAM_BASE_URL", apiKeyEnv: "HIDREAM_API_TOKEN",
|
|
147
|
+
modelEnv: "HIDREAM_IMAGE_MODEL", defaultModel: "v2L", models: ["v2L", "v2.1-standard"],
|
|
148
|
+
referenceModes: ["auto", "content"], maxReferences: 4, async: true,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
const definitionMap = new Map(definitions.map((definition) => [definition.id, definition]));
|
|
152
|
+
export function isExternalProvider(value) {
|
|
153
|
+
return definitionMap.has(value);
|
|
154
|
+
}
|
|
155
|
+
export function externalProviderDefinitions() {
|
|
156
|
+
return definitions;
|
|
157
|
+
}
|
|
158
|
+
function firstValue(...values) {
|
|
159
|
+
return values.map((value) => value?.trim()).find(Boolean);
|
|
160
|
+
}
|
|
161
|
+
function normalizeBaseUrl(value) {
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = new URL(value);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new Error("base_url must be a valid absolute HTTP(S) URL.");
|
|
168
|
+
}
|
|
169
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
170
|
+
throw new Error("base_url must use HTTP or HTTPS.");
|
|
171
|
+
}
|
|
172
|
+
return value.replace(/\/+$/, "");
|
|
173
|
+
}
|
|
174
|
+
export function resolveExternalConfig(input) {
|
|
175
|
+
const definition = definitionMap.get(input.provider);
|
|
176
|
+
if (input.provider === "kling" && input.apiKey) {
|
|
177
|
+
throw new Error("Kling credentials must use KLING_ACCESS_KEY and KLING_SECRET_KEY environment variables.");
|
|
178
|
+
}
|
|
179
|
+
const apiKey = firstValue(input.apiKey, process.env[definition.apiKeyEnv]);
|
|
180
|
+
if (!apiKey)
|
|
181
|
+
throw new Error(`Missing ${definition.name} API credential: set ${definition.apiKeyEnv}.`);
|
|
182
|
+
if (input.provider === "kling" && !firstValue(process.env.KLING_SECRET_KEY)) {
|
|
183
|
+
throw new Error("Missing Kling API credential: set KLING_SECRET_KEY.");
|
|
184
|
+
}
|
|
185
|
+
const model = firstValue(input.model, process.env[definition.modelEnv], definition.defaultModel);
|
|
186
|
+
if (!definition.models.includes(model)) {
|
|
187
|
+
throw new Error(`Unsupported ${definition.name} model '${model}'. Supported models: ${definition.models.join(", ")}.`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
provider: input.provider,
|
|
191
|
+
apiKey,
|
|
192
|
+
model,
|
|
193
|
+
baseUrl: normalizeBaseUrl(firstValue(input.baseUrl, process.env[definition.baseUrlEnv], definition.baseUrl)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function parseSize(size) {
|
|
197
|
+
const match = /^(\d+)x(\d+)$/.exec(size);
|
|
198
|
+
if (!match)
|
|
199
|
+
throw new Error("size must use WIDTHxHEIGHT, for example 1024x1024.");
|
|
200
|
+
const width = Number(match[1]);
|
|
201
|
+
const height = Number(match[2]);
|
|
202
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1) {
|
|
203
|
+
throw new Error("size width and height must be positive safe integers.");
|
|
204
|
+
}
|
|
205
|
+
return { width, height };
|
|
206
|
+
}
|
|
207
|
+
function gcd(a, b) {
|
|
208
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
209
|
+
}
|
|
210
|
+
function ratioFor(input) {
|
|
211
|
+
if (input.aspectRatio)
|
|
212
|
+
return input.aspectRatio;
|
|
213
|
+
const { width, height } = parseSize(input.size);
|
|
214
|
+
const divisor = gcd(width, height);
|
|
215
|
+
return `${width / divisor}:${height / divisor}`;
|
|
216
|
+
}
|
|
217
|
+
function dataUri(image) {
|
|
218
|
+
return `data:${image.mimeType};base64,${image.data.toString("base64")}`;
|
|
219
|
+
}
|
|
220
|
+
function validateReferences(input, definition) {
|
|
221
|
+
const count = input.inputImages?.length ?? 0;
|
|
222
|
+
if (count > definition.maxReferences) {
|
|
223
|
+
throw new Error(`${definition.name} supports at most ${definition.maxReferences} reference image${definition.maxReferences === 1 ? "" : "s"}.`);
|
|
224
|
+
}
|
|
225
|
+
if (count === 0)
|
|
226
|
+
return;
|
|
227
|
+
const mode = input.referenceMode ?? "auto";
|
|
228
|
+
if (!definition.referenceModes.includes(mode)) {
|
|
229
|
+
throw new Error(`${definition.name} does not support reference_mode '${mode}'. Supported modes: ${definition.referenceModes.join(", ")}.`);
|
|
230
|
+
}
|
|
231
|
+
if (mode === "auto" && !definition.referenceModes.includes("auto")) {
|
|
232
|
+
throw new Error(`${definition.name} requires an explicit reference_mode: ${definition.referenceModes.join(" or ")}.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function validateCommonOptions(input) {
|
|
236
|
+
const provider = input.config.provider;
|
|
237
|
+
if (input.quality !== "auto") {
|
|
238
|
+
const ideogramQuality = provider === "ideogram" && input.config.model === "p-image-ideogram";
|
|
239
|
+
if (provider !== "xai" && !ideogramQuality) {
|
|
240
|
+
throw new Error(`${definitionMap.get(provider).name} does not expose the common quality option; choose a provider model tier instead.`);
|
|
241
|
+
}
|
|
242
|
+
if (provider === "xai" && !new Set(["low", "medium"]).has(input.quality)) {
|
|
243
|
+
throw new Error("xAI quality must be auto, low, or medium.");
|
|
244
|
+
}
|
|
245
|
+
if (ideogramQuality && !new Set(["low", "medium", "high"]).has(input.quality)) {
|
|
246
|
+
throw new Error("Ideogram P-Image quality must be auto, low, medium, or high.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (input.outputFormat === "webp" && new Set(["luma", "runway", "bria", "freepik", "alibaba", "kling", "hidream"]).has(provider)) {
|
|
250
|
+
throw new Error(`${definitionMap.get(provider).name} does not support WebP output.`);
|
|
251
|
+
}
|
|
252
|
+
if (provider === "freepik" && input.outputFormat && input.outputFormat !== "png") {
|
|
253
|
+
throw new Error("Freepik Mystic output_format must be png.");
|
|
254
|
+
}
|
|
255
|
+
if (provider === "runway" && input.config.model === "gen4_image_turbo" && !input.inputImages?.length) {
|
|
256
|
+
throw new Error("Runway gen4_image_turbo requires at least one reference image.");
|
|
257
|
+
}
|
|
258
|
+
if (input.negativePrompt) {
|
|
259
|
+
const supportsNegative = new Set([
|
|
260
|
+
"stability", "playground", "baidu", "leonardo", "bria", "alibaba", "hidream",
|
|
261
|
+
]);
|
|
262
|
+
const modelSupportsNegative = provider === "ideogram" && input.config.model === "ideogram-v3"
|
|
263
|
+
|| provider === "recraft" && new Set(["recraftv3", "recraftv2"]).has(input.config.model);
|
|
264
|
+
if (!supportsNegative.has(provider) && !modelSupportsNegative) {
|
|
265
|
+
throw new Error(`${definitionMap.get(provider).name} model ${input.config.model} does not support negative_prompt.`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (input.seed !== undefined) {
|
|
269
|
+
const supportsSeed = new Set([
|
|
270
|
+
"stability", "ideogram", "recraft", "tencent", "baidu", "playground", "bfl", "krea",
|
|
271
|
+
"runway", "leonardo", "bria", "alibaba", "volcengine", "kling",
|
|
272
|
+
]);
|
|
273
|
+
if (!supportsSeed.has(provider)) {
|
|
274
|
+
throw new Error(`${definitionMap.get(provider).name} does not support the common seed option.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function detectMimeType(data) {
|
|
279
|
+
if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
|
|
280
|
+
return "image/png";
|
|
281
|
+
if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
|
|
282
|
+
return "image/jpeg";
|
|
283
|
+
if (data.length >= 12 && data.subarray(0, 4).toString("ascii") === "RIFF" && data.subarray(8, 12).toString("ascii") === "WEBP")
|
|
284
|
+
return "image/webp";
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
function decodeImage(value, label) {
|
|
288
|
+
const encoded = value.startsWith("data:") ? value.slice(value.indexOf(",") + 1) : value;
|
|
289
|
+
const normalized = encoded.replace(/\s+/g, "");
|
|
290
|
+
if (!normalized || !/^[A-Za-z0-9+/]+={0,2}$/.test(normalized))
|
|
291
|
+
return undefined;
|
|
292
|
+
const data = Buffer.from(normalized, "base64");
|
|
293
|
+
if (data.length === 0 || data.length > 50 * 1024 * 1024)
|
|
294
|
+
return undefined;
|
|
295
|
+
const mimeType = detectMimeType(data);
|
|
296
|
+
if (!mimeType)
|
|
297
|
+
return undefined;
|
|
298
|
+
if (data.toString("base64").replace(/=+$/, "") !== normalized.replace(/=+$/, "")) {
|
|
299
|
+
throw new Error(`${label} response contained invalid base64 image data.`);
|
|
300
|
+
}
|
|
301
|
+
return { data, mimeType };
|
|
302
|
+
}
|
|
303
|
+
const urlSchema = z.string().url();
|
|
304
|
+
function findImageValue(value, key = "") {
|
|
305
|
+
if (typeof value === "string") {
|
|
306
|
+
if (/poll|status|task|webhook/i.test(key))
|
|
307
|
+
return undefined;
|
|
308
|
+
if (/^(https?:\/\/)/.test(value) && urlSchema.safeParse(value).success)
|
|
309
|
+
return { url: value };
|
|
310
|
+
if (/base64|b64|image/i.test(key) || value.startsWith("data:image/"))
|
|
311
|
+
return { base64: value };
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
if (Array.isArray(value)) {
|
|
315
|
+
for (const item of value) {
|
|
316
|
+
const found = findImageValue(item, key);
|
|
317
|
+
if (found)
|
|
318
|
+
return found;
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
if (value && typeof value === "object") {
|
|
323
|
+
const entries = Object.entries(value);
|
|
324
|
+
const priority = entries.sort(([a], [b]) => Number(/url|image|sample|output|base64|b64/i.test(b)) - Number(/url|image|sample|output|base64|b64/i.test(a)));
|
|
325
|
+
for (const [childKey, childValue] of priority) {
|
|
326
|
+
const found = findImageValue(childValue, childKey);
|
|
327
|
+
if (found)
|
|
328
|
+
return found;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
async function imageFromPayload(payload, label, fetchImpl) {
|
|
334
|
+
const found = findImageValue(payload);
|
|
335
|
+
if (!found)
|
|
336
|
+
return undefined;
|
|
337
|
+
if (found.base64) {
|
|
338
|
+
const decoded = decodeImage(found.base64, label);
|
|
339
|
+
if (decoded)
|
|
340
|
+
return decoded;
|
|
341
|
+
}
|
|
342
|
+
if (found.url) {
|
|
343
|
+
const downloaded = await loadRemoteImage(found.url, fetchImpl, undefined, undefined, false);
|
|
344
|
+
return { data: downloaded.data, mimeType: downloaded.mimeType };
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
async function jsonResponse(response, label) {
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
const detail = (await response.text()).slice(0, 500);
|
|
351
|
+
throw new Error(`${label} image request failed (${response.status}): ${detail || response.statusText}`);
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
return await response.json();
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
throw new Error(`${label} image response was not valid JSON.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function bearer(key) {
|
|
361
|
+
return { authorization: `Bearer ${key}`, "content-type": "application/json" };
|
|
362
|
+
}
|
|
363
|
+
async function requestGoogle(input) {
|
|
364
|
+
const parts = [{ text: input.prompt }];
|
|
365
|
+
for (const image of input.inputImages ?? []) {
|
|
366
|
+
parts.push({ inlineData: { mimeType: image.mimeType, data: image.data.toString("base64") } });
|
|
367
|
+
}
|
|
368
|
+
const body = {
|
|
369
|
+
contents: [{ role: "user", parts }],
|
|
370
|
+
generationConfig: {
|
|
371
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
372
|
+
imageConfig: { aspectRatio: ratioFor(input), ...(input.resolution ? { imageSize: input.resolution } : {}) },
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}/models/${input.config.model}:generateContent`, {
|
|
376
|
+
method: "POST",
|
|
377
|
+
headers: { "x-goog-api-key": input.config.apiKey, "content-type": "application/json" },
|
|
378
|
+
body: JSON.stringify(body),
|
|
379
|
+
signal: AbortSignal.timeout(120_000),
|
|
380
|
+
});
|
|
381
|
+
const payload = await jsonResponse(response, "Google Gemini");
|
|
382
|
+
const googleImage = z.object({
|
|
383
|
+
candidates: z.array(z.object({
|
|
384
|
+
content: z.object({
|
|
385
|
+
parts: z.array(z.object({
|
|
386
|
+
inlineData: z.object({ data: z.string() }).optional(),
|
|
387
|
+
}).passthrough()),
|
|
388
|
+
}),
|
|
389
|
+
})).min(1),
|
|
390
|
+
}).safeParse(payload);
|
|
391
|
+
const inlineData = googleImage.success
|
|
392
|
+
? googleImage.data.candidates.flatMap((candidate) => candidate.content.parts)
|
|
393
|
+
.map((part) => part.inlineData?.data).find(Boolean)
|
|
394
|
+
: undefined;
|
|
395
|
+
if (inlineData) {
|
|
396
|
+
const decoded = decodeImage(inlineData, "Google Gemini");
|
|
397
|
+
if (decoded)
|
|
398
|
+
return decoded;
|
|
399
|
+
}
|
|
400
|
+
const image = await imageFromPayload(payload, "Google Gemini", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
401
|
+
if (!image)
|
|
402
|
+
throw new Error("Google Gemini image response contained no valid image data.");
|
|
403
|
+
return image;
|
|
404
|
+
}
|
|
405
|
+
async function requestXai(input) {
|
|
406
|
+
const hasReferences = Boolean(input.inputImages?.length);
|
|
407
|
+
const body = {
|
|
408
|
+
model: input.config.model,
|
|
409
|
+
prompt: input.prompt,
|
|
410
|
+
n: 1,
|
|
411
|
+
aspect_ratio: input.aspectRatio ?? ratioFor(input),
|
|
412
|
+
resolution: input.resolution?.toLowerCase() ?? "1k",
|
|
413
|
+
response_format: "b64_json",
|
|
414
|
+
};
|
|
415
|
+
if (input.quality !== "auto")
|
|
416
|
+
body.quality = input.quality;
|
|
417
|
+
if (hasReferences) {
|
|
418
|
+
body.image = { url: dataUri(input.inputImages[0]), type: "image_url" };
|
|
419
|
+
}
|
|
420
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}/images/${hasReferences ? "edits" : "generations"}`, {
|
|
421
|
+
method: "POST", headers: bearer(input.config.apiKey), body: JSON.stringify(body), signal: AbortSignal.timeout(120_000),
|
|
422
|
+
});
|
|
423
|
+
const payload = await jsonResponse(response, "xAI");
|
|
424
|
+
const image = await imageFromPayload(payload, "xAI", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
425
|
+
if (!image)
|
|
426
|
+
throw new Error("xAI image response contained no valid image data.");
|
|
427
|
+
return image;
|
|
428
|
+
}
|
|
429
|
+
async function requestStability(input) {
|
|
430
|
+
const form = new FormData();
|
|
431
|
+
form.append("prompt", input.prompt);
|
|
432
|
+
form.append("output_format", input.outputFormat ?? "png");
|
|
433
|
+
form.append("aspect_ratio", ratioFor(input));
|
|
434
|
+
if (input.negativePrompt)
|
|
435
|
+
form.append("negative_prompt", input.negativePrompt);
|
|
436
|
+
if (input.seed !== undefined)
|
|
437
|
+
form.append("seed", String(input.seed));
|
|
438
|
+
if (input.inputImages?.[0]) {
|
|
439
|
+
const image = input.inputImages[0];
|
|
440
|
+
form.append("image", new Blob([new Uint8Array(image.data)], { type: image.mimeType }), image.fileName);
|
|
441
|
+
form.append("strength", "0.7");
|
|
442
|
+
}
|
|
443
|
+
let path;
|
|
444
|
+
if (input.config.model === "stable-image-ultra")
|
|
445
|
+
path = "/v2beta/stable-image/generate/ultra";
|
|
446
|
+
else if (input.config.model === "stable-image-core")
|
|
447
|
+
path = "/v2beta/stable-image/generate/core";
|
|
448
|
+
else {
|
|
449
|
+
path = "/v2beta/stable-image/generate/sd3";
|
|
450
|
+
form.append("model", input.config.model);
|
|
451
|
+
}
|
|
452
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}${path}`, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: { authorization: `Bearer ${input.config.apiKey}`, accept: "application/json" },
|
|
455
|
+
body: form,
|
|
456
|
+
signal: AbortSignal.timeout(120_000),
|
|
457
|
+
});
|
|
458
|
+
const payload = await jsonResponse(response, "Stability AI");
|
|
459
|
+
const image = await imageFromPayload(payload, "Stability AI", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
460
|
+
if (!image)
|
|
461
|
+
throw new Error("Stability AI image response contained no valid image data.");
|
|
462
|
+
return image;
|
|
463
|
+
}
|
|
464
|
+
async function requestIdeogram(input) {
|
|
465
|
+
if (input.config.model !== "ideogram-v3" && input.inputImages?.length) {
|
|
466
|
+
throw new Error(`${input.config.model} generation does not support reference_images; use ideogram-v3.`);
|
|
467
|
+
}
|
|
468
|
+
const form = new FormData();
|
|
469
|
+
const isV4 = input.config.model === "ideogram-v4";
|
|
470
|
+
const isPImage = input.config.model === "p-image-ideogram";
|
|
471
|
+
form.append(isV4 ? "text_prompt" : "prompt", input.prompt);
|
|
472
|
+
if (isV4)
|
|
473
|
+
form.append("resolution", input.size);
|
|
474
|
+
else
|
|
475
|
+
form.append("aspect_ratio", ratioFor(input).replace(":", "x"));
|
|
476
|
+
if (isPImage)
|
|
477
|
+
form.append("resolution", input.resolution ?? "1K");
|
|
478
|
+
if (isPImage && input.quality !== "auto")
|
|
479
|
+
form.append("quality", input.quality.toUpperCase());
|
|
480
|
+
if (!isV4 && input.negativePrompt)
|
|
481
|
+
form.append("negative_prompt", input.negativePrompt);
|
|
482
|
+
if (!isV4 && input.seed !== undefined)
|
|
483
|
+
form.append("seed", String(input.seed));
|
|
484
|
+
const field = input.referenceMode === "character" ? "character_reference_images" : "style_reference_images";
|
|
485
|
+
for (const image of input.inputImages ?? []) {
|
|
486
|
+
form.append(field, new Blob([new Uint8Array(image.data)], { type: image.mimeType }), image.fileName);
|
|
487
|
+
}
|
|
488
|
+
const endpoint = isPImage ? "/v1/text-to-image/p-image-ideogram" : `/v1/ideogram-${isV4 ? "v4" : "v3"}/generate`;
|
|
489
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}${endpoint}`, {
|
|
490
|
+
method: "POST", headers: { "api-key": input.config.apiKey }, body: form, signal: AbortSignal.timeout(120_000),
|
|
491
|
+
});
|
|
492
|
+
const payload = await jsonResponse(response, "Ideogram");
|
|
493
|
+
const image = await imageFromPayload(payload, "Ideogram", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
494
|
+
if (!image)
|
|
495
|
+
throw new Error("Ideogram image response contained no valid image data.");
|
|
496
|
+
return image;
|
|
497
|
+
}
|
|
498
|
+
async function requestRecraft(input) {
|
|
499
|
+
const body = {
|
|
500
|
+
model: input.config.model,
|
|
501
|
+
prompt: input.prompt,
|
|
502
|
+
n: 1,
|
|
503
|
+
size: input.size,
|
|
504
|
+
response_format: "b64_json",
|
|
505
|
+
};
|
|
506
|
+
if (input.seed !== undefined)
|
|
507
|
+
body.random_seed = input.seed;
|
|
508
|
+
if (input.negativePrompt)
|
|
509
|
+
body.negative_prompt = input.negativePrompt;
|
|
510
|
+
if (input.inputImages?.length)
|
|
511
|
+
body.style_reference_urls = input.inputImages.map(dataUri);
|
|
512
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}/images/generations`, {
|
|
513
|
+
method: "POST", headers: bearer(input.config.apiKey), body: JSON.stringify(body), signal: AbortSignal.timeout(120_000),
|
|
514
|
+
});
|
|
515
|
+
const payload = await jsonResponse(response, "Recraft");
|
|
516
|
+
const image = await imageFromPayload(payload, "Recraft", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
517
|
+
if (!image)
|
|
518
|
+
throw new Error("Recraft image response contained no valid image data.");
|
|
519
|
+
return image;
|
|
520
|
+
}
|
|
521
|
+
async function requestTencent(input) {
|
|
522
|
+
const body = { model: input.config.model, prompt: input.prompt, size: input.size, n: 1 };
|
|
523
|
+
if (input.inputImages?.length)
|
|
524
|
+
body.images = input.inputImages.map(dataUri);
|
|
525
|
+
if (input.seed !== undefined)
|
|
526
|
+
body.seed = input.seed;
|
|
527
|
+
if (input.watermarkEnabled)
|
|
528
|
+
body.footnote = "AI generated";
|
|
529
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}/v1/wand/hunyuan-image/v3-generation`, {
|
|
530
|
+
method: "POST", headers: bearer(input.config.apiKey), body: JSON.stringify(body), signal: AbortSignal.timeout(120_000),
|
|
531
|
+
});
|
|
532
|
+
const payload = await jsonResponse(response, "Tencent Hunyuan");
|
|
533
|
+
const image = await imageFromPayload(payload, "Tencent Hunyuan", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
534
|
+
if (!image)
|
|
535
|
+
throw new Error("Tencent Hunyuan image response contained no valid image data.");
|
|
536
|
+
return image;
|
|
537
|
+
}
|
|
538
|
+
async function requestBaidu(input) {
|
|
539
|
+
const body = { model: input.config.model, prompt: input.prompt, n: 1, size: input.size };
|
|
540
|
+
if (input.negativePrompt)
|
|
541
|
+
body.negative_prompt = input.negativePrompt;
|
|
542
|
+
if (input.seed !== undefined)
|
|
543
|
+
body.seed = input.seed;
|
|
544
|
+
if (input.watermarkEnabled !== undefined)
|
|
545
|
+
body.watermark = input.watermarkEnabled;
|
|
546
|
+
if (input.inputImages?.[0])
|
|
547
|
+
body.image = dataUri(input.inputImages[0]);
|
|
548
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}/v2/images/generations`, {
|
|
549
|
+
method: "POST", headers: bearer(input.config.apiKey), body: JSON.stringify(body), signal: AbortSignal.timeout(120_000),
|
|
550
|
+
});
|
|
551
|
+
const payload = await jsonResponse(response, "Baidu Qianfan");
|
|
552
|
+
const image = await imageFromPayload(payload, "Baidu Qianfan", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
553
|
+
if (!image)
|
|
554
|
+
throw new Error("Baidu Qianfan image response contained no valid image data.");
|
|
555
|
+
return image;
|
|
556
|
+
}
|
|
557
|
+
async function requestPlayground(input) {
|
|
558
|
+
const { width, height } = parseSize(input.size);
|
|
559
|
+
const body = { prompt: input.prompt, filter_model: input.config.model, width, height };
|
|
560
|
+
if (input.negativePrompt)
|
|
561
|
+
body.negative_prompt = input.negativePrompt;
|
|
562
|
+
if (input.seed !== undefined)
|
|
563
|
+
body.seed = input.seed;
|
|
564
|
+
if (input.inputImages?.[0])
|
|
565
|
+
body.init_image = input.inputImages[0].data.toString("base64");
|
|
566
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}/api/models/external/v1`, {
|
|
567
|
+
method: "POST", headers: bearer(input.config.apiKey), body: JSON.stringify(body), signal: AbortSignal.timeout(120_000),
|
|
568
|
+
});
|
|
569
|
+
const payload = await jsonResponse(response, "Playground AI");
|
|
570
|
+
const image = await imageFromPayload(payload, "Playground AI", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
571
|
+
if (!image)
|
|
572
|
+
throw new Error("Playground AI image response contained no valid image data.");
|
|
573
|
+
return image;
|
|
574
|
+
}
|
|
575
|
+
function nestedString(payload, paths) {
|
|
576
|
+
for (const path of paths) {
|
|
577
|
+
let value = payload;
|
|
578
|
+
for (const part of path) {
|
|
579
|
+
if (!value || typeof value !== "object") {
|
|
580
|
+
value = undefined;
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
value = value[part];
|
|
584
|
+
}
|
|
585
|
+
if (typeof value === "string" && value)
|
|
586
|
+
return value;
|
|
587
|
+
}
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
function statusOf(payload) {
|
|
591
|
+
return nestedString(payload, [
|
|
592
|
+
["status"], ["state"], ["task_status"], ["output", "task_status"], ["data", "status"],
|
|
593
|
+
["data", "task_status"], ["output", "status"], ["result", "status"], ["result", "state"],
|
|
594
|
+
["result", "task_status"], ["result", "sub_task_results", "0", "task_status"],
|
|
595
|
+
])?.toLowerCase();
|
|
596
|
+
}
|
|
597
|
+
function asyncSettings() {
|
|
598
|
+
const bounded = (name, fallback, min, max) => {
|
|
599
|
+
const raw = process.env[name];
|
|
600
|
+
if (!raw)
|
|
601
|
+
return fallback;
|
|
602
|
+
const parsed = Number(raw);
|
|
603
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
604
|
+
throw new Error(`${name} must be an integer between ${min} and ${max}.`);
|
|
605
|
+
}
|
|
606
|
+
return parsed;
|
|
607
|
+
};
|
|
608
|
+
return {
|
|
609
|
+
timeoutMs: bounded("IMAGEFORGE_ASYNC_TIMEOUT_MS", 300_000, 30_000, 600_000),
|
|
610
|
+
intervalMs: bounded("IMAGEFORGE_POLL_INTERVAL_MS", 2_000, 500, 10_000),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function delay(ms) {
|
|
614
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
615
|
+
}
|
|
616
|
+
async function submitAndPoll(input, options) {
|
|
617
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
618
|
+
const response = await fetchImpl(options.submitUrl, {
|
|
619
|
+
method: "POST",
|
|
620
|
+
headers: options.submitHeaders,
|
|
621
|
+
body: JSON.stringify(options.body),
|
|
622
|
+
signal: AbortSignal.timeout(120_000),
|
|
623
|
+
});
|
|
624
|
+
let payload = await jsonResponse(response, options.label);
|
|
625
|
+
const downloadFetch = input.downloadFetchImpl ?? fetchImpl;
|
|
626
|
+
const immediateImage = await imageFromPayload(payload, options.label, downloadFetch);
|
|
627
|
+
if (immediateImage)
|
|
628
|
+
return immediateImage;
|
|
629
|
+
const statusUrl = options.statusUrl(payload);
|
|
630
|
+
if (!statusUrl)
|
|
631
|
+
throw new Error(`${options.label} image response contained no task ID or status URL.`);
|
|
632
|
+
const { timeoutMs, intervalMs } = asyncSettings();
|
|
633
|
+
const deadline = Date.now() + timeoutMs;
|
|
634
|
+
let nextDelayMs = intervalMs;
|
|
635
|
+
while (Date.now() < deadline) {
|
|
636
|
+
const status = statusOf(payload);
|
|
637
|
+
if (status && ["fail", "failed", "error", "unknown", "canceled", "cancelled", "rejected"].includes(status)) {
|
|
638
|
+
throw new Error(`${options.label} image task failed with status '${status}'.`);
|
|
639
|
+
}
|
|
640
|
+
const image = await imageFromPayload(payload, options.label, downloadFetch);
|
|
641
|
+
if (image)
|
|
642
|
+
return image;
|
|
643
|
+
await delay(nextDelayMs);
|
|
644
|
+
const pollResponse = await fetchImpl(statusUrl, {
|
|
645
|
+
method: "GET",
|
|
646
|
+
headers: options.statusHeaders ?? options.submitHeaders,
|
|
647
|
+
signal: AbortSignal.timeout(Math.min(120_000, Math.max(1, deadline - Date.now()))),
|
|
648
|
+
});
|
|
649
|
+
const retryAfter = pollResponse.headers.get("retry-after");
|
|
650
|
+
const retryAfterSeconds = retryAfter === null ? undefined : Number(retryAfter);
|
|
651
|
+
nextDelayMs = retryAfterSeconds !== undefined && Number.isFinite(retryAfterSeconds)
|
|
652
|
+
? Math.min(10_000, Math.max(500, retryAfterSeconds * 1000))
|
|
653
|
+
: intervalMs;
|
|
654
|
+
payload = await jsonResponse(pollResponse, options.label);
|
|
655
|
+
}
|
|
656
|
+
throw new Error(`${options.label} image task timed out after ${timeoutMs} ms.`);
|
|
657
|
+
}
|
|
658
|
+
function taskId(payload) {
|
|
659
|
+
return nestedString(payload, [
|
|
660
|
+
["id"], ["job_id"], ["task_id"], ["request_id"], ["generation_id"], ["generationId"],
|
|
661
|
+
["output", "task_id"], ["data", "task_id"], ["data", "job_id"], ["data", "generation_id"],
|
|
662
|
+
["data", "id"], ["result", "task_id"], ["result", "id"], ["sdGenerationJob", "generationId"],
|
|
663
|
+
]);
|
|
664
|
+
}
|
|
665
|
+
function absoluteStatusUrl(baseUrl, value) {
|
|
666
|
+
if (!value)
|
|
667
|
+
return undefined;
|
|
668
|
+
if (/^https?:\/\//.test(value))
|
|
669
|
+
return value;
|
|
670
|
+
return `${baseUrl}/${value.replace(/^\/+/, "")}`;
|
|
671
|
+
}
|
|
672
|
+
function klingToken(accessKey) {
|
|
673
|
+
const secret = process.env.KLING_SECRET_KEY;
|
|
674
|
+
const now = Math.floor(Date.now() / 1000);
|
|
675
|
+
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
676
|
+
const signingInput = `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ iss: accessKey, exp: now + 1800, nbf: now - 5 })}`;
|
|
677
|
+
const signature = createHmac("sha256", secret).update(signingInput).digest("base64url");
|
|
678
|
+
return `${signingInput}.${signature}`;
|
|
679
|
+
}
|
|
680
|
+
async function requestAsyncProvider(input) {
|
|
681
|
+
const provider = input.config.provider;
|
|
682
|
+
let body;
|
|
683
|
+
let submitUrl;
|
|
684
|
+
let headers = bearer(input.config.apiKey);
|
|
685
|
+
let statusBuilder;
|
|
686
|
+
if (provider === "bfl") {
|
|
687
|
+
const { width, height } = parseSize(input.size);
|
|
688
|
+
body = { prompt: input.prompt, width, height, output_format: input.outputFormat ?? "png" };
|
|
689
|
+
if (input.seed !== undefined)
|
|
690
|
+
body.seed = input.seed;
|
|
691
|
+
submitUrl = `${input.config.baseUrl}/v1/${input.config.model}`;
|
|
692
|
+
headers = { "x-key": input.config.apiKey, "content-type": "application/json" };
|
|
693
|
+
(input.inputImages ?? []).forEach((image, index) => {
|
|
694
|
+
body[index === 0 ? "input_image" : `input_image_${index + 1}`] = dataUri(image);
|
|
695
|
+
});
|
|
696
|
+
statusBuilder = (payload) => absoluteStatusUrl(input.config.baseUrl, nestedString(payload, [["polling_url"]]))
|
|
697
|
+
?? (taskId(payload) ? `${input.config.baseUrl}/v1/get_result?id=${encodeURIComponent(taskId(payload))}` : undefined);
|
|
698
|
+
}
|
|
699
|
+
else if (provider === "luma") {
|
|
700
|
+
body = {
|
|
701
|
+
prompt: input.prompt,
|
|
702
|
+
model: input.config.model,
|
|
703
|
+
type: "image",
|
|
704
|
+
aspect_ratio: ratioFor(input),
|
|
705
|
+
output_format: input.outputFormat === "webp" ? undefined : input.outputFormat,
|
|
706
|
+
image_ref: input.inputImages?.map((image) => ({ data: image.data.toString("base64"), media_type: image.mimeType })),
|
|
707
|
+
};
|
|
708
|
+
submitUrl = `${input.config.baseUrl}/generations`;
|
|
709
|
+
statusBuilder = (payload) => absoluteStatusUrl(input.config.baseUrl, nestedString(payload, [["status_url"]]))
|
|
710
|
+
?? (taskId(payload) ? `${input.config.baseUrl}/generations/${taskId(payload)}` : undefined);
|
|
711
|
+
}
|
|
712
|
+
else if (provider === "krea") {
|
|
713
|
+
body = {
|
|
714
|
+
prompt: input.prompt,
|
|
715
|
+
aspect_ratio: ratioFor(input),
|
|
716
|
+
resolution: input.resolution ?? "1K",
|
|
717
|
+
image_style_references: input.inputImages?.map(dataUri),
|
|
718
|
+
};
|
|
719
|
+
if (input.seed !== undefined)
|
|
720
|
+
body.seed = input.seed;
|
|
721
|
+
submitUrl = `${input.config.baseUrl}/generate/image/krea/${input.config.model}`;
|
|
722
|
+
statusBuilder = (payload) => absoluteStatusUrl(input.config.baseUrl, nestedString(payload, [["status_url"], ["urls", "status"]]))
|
|
723
|
+
?? (taskId(payload) ? `${input.config.baseUrl}/jobs/${taskId(payload)}` : undefined);
|
|
724
|
+
}
|
|
725
|
+
else if (provider === "runway") {
|
|
726
|
+
body = {
|
|
727
|
+
model: input.config.model,
|
|
728
|
+
promptText: input.prompt,
|
|
729
|
+
ratio: input.size.replace("x", ":"),
|
|
730
|
+
referenceImages: input.inputImages?.map((image) => ({ uri: dataUri(image) })),
|
|
731
|
+
};
|
|
732
|
+
if (input.seed !== undefined)
|
|
733
|
+
body.seed = input.seed;
|
|
734
|
+
submitUrl = `${input.config.baseUrl}/v1/text_to_image`;
|
|
735
|
+
headers = { ...bearer(input.config.apiKey), "x-runway-version": "2024-11-06" };
|
|
736
|
+
statusBuilder = (payload) => taskId(payload) ? `${input.config.baseUrl}/v1/tasks/${taskId(payload)}` : undefined;
|
|
737
|
+
}
|
|
738
|
+
else if (provider === "leonardo") {
|
|
739
|
+
const { width, height } = parseSize(input.size);
|
|
740
|
+
body = { model: input.config.model, prompt: input.prompt, width, height, num_images: 1 };
|
|
741
|
+
if (input.seed !== undefined)
|
|
742
|
+
body.seed = input.seed;
|
|
743
|
+
if (input.negativePrompt)
|
|
744
|
+
body.negative_prompt = input.negativePrompt;
|
|
745
|
+
submitUrl = `${input.config.baseUrl}/generations`;
|
|
746
|
+
body.imageReferences = input.inputImages?.map((image) => ({ image: dataUri(image), type: input.referenceMode ?? "content" }));
|
|
747
|
+
statusBuilder = (payload) => taskId(payload) ? `${input.config.baseUrl}/generations/${taskId(payload)}` : undefined;
|
|
748
|
+
}
|
|
749
|
+
else if (provider === "bria") {
|
|
750
|
+
body = {
|
|
751
|
+
prompt: input.prompt,
|
|
752
|
+
images: input.inputImages?.map(dataUri),
|
|
753
|
+
aspect_ratio: ratioFor(input),
|
|
754
|
+
seed: input.seed,
|
|
755
|
+
negative_prompt: input.negativePrompt,
|
|
756
|
+
output_type: input.outputFormat === "webp" ? undefined : input.outputFormat,
|
|
757
|
+
sync: false,
|
|
758
|
+
};
|
|
759
|
+
submitUrl = `${input.config.baseUrl}/image/${input.config.model === "fibo-lite" ? "generate/lite" : "generate"}`;
|
|
760
|
+
headers = { api_token: input.config.apiKey, "content-type": "application/json", "user-agent": "BriaPlatform/Sandbox/LLMsAgent" };
|
|
761
|
+
statusBuilder = (payload) => absoluteStatusUrl(input.config.baseUrl, nestedString(payload, [["status_url"]]))
|
|
762
|
+
?? (taskId(payload) ? `${input.config.baseUrl}/status/${taskId(payload)}` : undefined);
|
|
763
|
+
}
|
|
764
|
+
else if (provider === "freepik") {
|
|
765
|
+
body = { prompt: input.prompt, model: input.config.model };
|
|
766
|
+
submitUrl = `${input.config.baseUrl}/v1/ai/mystic`;
|
|
767
|
+
headers = { "x-magnific-api-key": input.config.apiKey, "content-type": "application/json" };
|
|
768
|
+
const reference = input.inputImages?.[0]?.data.toString("base64");
|
|
769
|
+
if (reference) {
|
|
770
|
+
body[input.referenceMode === "style" ? "style_reference" : "structure_reference"] = reference;
|
|
771
|
+
}
|
|
772
|
+
body.resolution = input.resolution?.toLowerCase() ?? "1k";
|
|
773
|
+
body.aspect_ratio = {
|
|
774
|
+
"1:1": "square_1_1", "4:3": "classic_4_3", "3:4": "traditional_3_4",
|
|
775
|
+
"16:9": "widescreen_16_9", "9:16": "social_story_9_16", "3:2": "standard_3_2",
|
|
776
|
+
"2:3": "portrait_2_3", "2:1": "horizontal_2_1", "1:2": "vertical_1_2",
|
|
777
|
+
}[ratioFor(input)];
|
|
778
|
+
if (!body.aspect_ratio)
|
|
779
|
+
throw new Error(`Freepik Mystic does not support aspect ratio ${ratioFor(input)}.`);
|
|
780
|
+
statusBuilder = (payload) => taskId(payload) ? `${input.config.baseUrl}/v1/ai/mystic/${taskId(payload)}` : undefined;
|
|
781
|
+
}
|
|
782
|
+
else if (provider === "alibaba") {
|
|
783
|
+
body = { model: input.config.model };
|
|
784
|
+
submitUrl = `${input.config.baseUrl}/api/v1/services/aigc/multimodal-generation/generation`;
|
|
785
|
+
headers = { ...bearer(input.config.apiKey), "x-dashscope-async": "enable" };
|
|
786
|
+
const content = [
|
|
787
|
+
...(input.inputImages ?? []).map((image) => ({ image: dataUri(image) })),
|
|
788
|
+
{ text: input.prompt },
|
|
789
|
+
];
|
|
790
|
+
Object.assign(body, { input: { messages: [{ role: "user", content }] }, parameters: {
|
|
791
|
+
n: 1, size: input.size.replace("x", "*"), watermark: input.watermarkEnabled,
|
|
792
|
+
negative_prompt: input.negativePrompt, seed: input.seed,
|
|
793
|
+
} });
|
|
794
|
+
statusBuilder = (payload) => taskId(payload) ? `${input.config.baseUrl}/api/v1/tasks/${taskId(payload)}` : undefined;
|
|
795
|
+
}
|
|
796
|
+
else if (provider === "kling") {
|
|
797
|
+
body = {
|
|
798
|
+
model_name: input.config.model,
|
|
799
|
+
prompt: input.prompt,
|
|
800
|
+
n: 1,
|
|
801
|
+
aspect_ratio: input.aspectRatio ?? ratioFor(input),
|
|
802
|
+
image_list: input.inputImages?.map((image) => ({ image: dataUri(image), type: input.referenceMode ?? "content" })),
|
|
803
|
+
};
|
|
804
|
+
submitUrl = `${input.config.baseUrl}/v1/images/generations`;
|
|
805
|
+
headers = bearer(klingToken(input.config.apiKey));
|
|
806
|
+
statusBuilder = (payload) => taskId(payload) ? `${input.config.baseUrl}/v1/images/generations/${taskId(payload)}` : undefined;
|
|
807
|
+
}
|
|
808
|
+
else if (provider === "hidream") {
|
|
809
|
+
const references = input.inputImages ?? [];
|
|
810
|
+
body = {
|
|
811
|
+
prompt: input.prompt,
|
|
812
|
+
negative_prompt: input.negativePrompt,
|
|
813
|
+
version: input.config.model,
|
|
814
|
+
resolution: input.size.replace("x", "*"),
|
|
815
|
+
img_count: 1,
|
|
816
|
+
image: references.map(dataUri),
|
|
817
|
+
};
|
|
818
|
+
submitUrl = `${input.config.baseUrl}/api-pub/gw/v3/image/${references.length ? "img2img" : "txt2img"}/async`;
|
|
819
|
+
statusBuilder = (payload) => taskId(payload)
|
|
820
|
+
? `${input.config.baseUrl}/api-pub/gw/v3/image/${references.length ? "img2img" : "txt2img"}/async/results?task_id=${encodeURIComponent(taskId(payload))}`
|
|
821
|
+
: undefined;
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
throw new Error(`Unsupported async provider '${provider}'.`);
|
|
825
|
+
}
|
|
826
|
+
return submitAndPoll(input, {
|
|
827
|
+
label: definitionMap.get(provider).name,
|
|
828
|
+
submitUrl,
|
|
829
|
+
submitHeaders: headers,
|
|
830
|
+
body,
|
|
831
|
+
statusUrl: statusBuilder,
|
|
832
|
+
statusHeaders: headers,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
async function requestVolcengine(input) {
|
|
836
|
+
const body = {
|
|
837
|
+
model: input.config.model,
|
|
838
|
+
prompt: input.prompt,
|
|
839
|
+
size: input.size,
|
|
840
|
+
response_format: "url",
|
|
841
|
+
watermark: input.watermarkEnabled,
|
|
842
|
+
seed: input.seed,
|
|
843
|
+
};
|
|
844
|
+
if (input.inputImages?.length)
|
|
845
|
+
body.image = input.inputImages.map(dataUri);
|
|
846
|
+
const response = await (input.fetchImpl ?? fetch)(`${input.config.baseUrl}/images/generations`, {
|
|
847
|
+
method: "POST", headers: bearer(input.config.apiKey), body: JSON.stringify(body), signal: AbortSignal.timeout(120_000),
|
|
848
|
+
});
|
|
849
|
+
const payload = await jsonResponse(response, "Volcengine Ark");
|
|
850
|
+
const image = await imageFromPayload(payload, "Volcengine Ark", input.downloadFetchImpl ?? input.fetchImpl ?? fetch);
|
|
851
|
+
if (!image)
|
|
852
|
+
throw new Error("Volcengine Ark image response contained no valid image data.");
|
|
853
|
+
return image;
|
|
854
|
+
}
|
|
855
|
+
export async function requestExternalImage(input) {
|
|
856
|
+
const definition = definitionMap.get(input.config.provider);
|
|
857
|
+
validateReferences(input, definition);
|
|
858
|
+
validateCommonOptions(input);
|
|
859
|
+
switch (input.config.provider) {
|
|
860
|
+
case "google": return requestGoogle(input);
|
|
861
|
+
case "xai": return requestXai(input);
|
|
862
|
+
case "stability": return requestStability(input);
|
|
863
|
+
case "ideogram": return requestIdeogram(input);
|
|
864
|
+
case "recraft": return requestRecraft(input);
|
|
865
|
+
case "tencent": return requestTencent(input);
|
|
866
|
+
case "baidu": return requestBaidu(input);
|
|
867
|
+
case "playground": return requestPlayground(input);
|
|
868
|
+
case "volcengine": return requestVolcengine(input);
|
|
869
|
+
default: return requestAsyncProvider(input);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
export function externalCapabilities() {
|
|
873
|
+
return definitions.map((definition) => ({
|
|
874
|
+
provider: definition.id,
|
|
875
|
+
name: definition.name,
|
|
876
|
+
default_model: definition.defaultModel,
|
|
877
|
+
models: [...definition.models],
|
|
878
|
+
reference_modes: [...definition.referenceModes],
|
|
879
|
+
max_reference_images: definition.maxReferences,
|
|
880
|
+
async: definition.async,
|
|
881
|
+
credential_environment_variables: definition.id === "kling"
|
|
882
|
+
? [definition.apiKeyEnv, "KLING_SECRET_KEY"]
|
|
883
|
+
: [definition.apiKeyEnv],
|
|
884
|
+
base_url_environment_variable: definition.baseUrlEnv,
|
|
885
|
+
model_environment_variable: definition.modelEnv,
|
|
886
|
+
docs_url: definition.docsUrl,
|
|
887
|
+
implementation_basis: "official-docs-contract-tests",
|
|
888
|
+
live_verified: false,
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
//# sourceMappingURL=externalProviders.js.map
|