Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
362
extensions/openrouter/image-generation-provider.ts
Normal file
362
extensions/openrouter/image-generation-provider.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
// Openrouter provider module implements model/runtime integration.
|
||||
import type {
|
||||
GeneratedImageAsset,
|
||||
ImageGenerationProvider,
|
||||
ImageGenerationRequest,
|
||||
} from "openclaw/plugin-sdk/image-generation";
|
||||
import {
|
||||
generatedImageAssetFromBase64,
|
||||
generatedImageAssetFromDataUrl,
|
||||
resolveInlineImageJsonResponseMaxBytes,
|
||||
toImageDataUrl,
|
||||
} from "openclaw/plugin-sdk/image-generation";
|
||||
import { MAX_IMAGE_BYTES } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
postJsonRequest,
|
||||
readProviderJsonResponse,
|
||||
resolveProviderHttpRequestConfig,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { OPENROUTER_BASE_URL } from "./provider-catalog.js";
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3.1-flash-image-preview";
|
||||
const DEFAULT_TIMEOUT_MS = 180_000;
|
||||
const MAX_IMAGE_RESULTS = 4;
|
||||
const MB = 1024 * 1024;
|
||||
const SUPPORTED_MODELS = [
|
||||
DEFAULT_MODEL,
|
||||
"google/gemini-3-pro-image-preview",
|
||||
"openai/gpt-5.4-image-2",
|
||||
] as const;
|
||||
const SUPPORTED_ASPECT_RATIOS = [
|
||||
"1:1",
|
||||
"2:3",
|
||||
"3:2",
|
||||
"3:4",
|
||||
"4:3",
|
||||
"4:5",
|
||||
"5:4",
|
||||
"9:16",
|
||||
"16:9",
|
||||
"21:9",
|
||||
] as const;
|
||||
const OPENROUTER_IMAGE_MALFORMED_RESPONSE = "OpenRouter image generation response malformed";
|
||||
|
||||
function throwMalformedOpenRouterImageResponse(message: string | undefined): never | undefined {
|
||||
if (message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function pushDataUrlImage(
|
||||
images: GeneratedImageAsset[],
|
||||
dataUrl: string,
|
||||
malformedResponseError?: string,
|
||||
): void {
|
||||
const image = generatedImageAssetFromDataUrl({ dataUrl, index: images.length });
|
||||
if (!image) {
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
images.push(image);
|
||||
}
|
||||
|
||||
function extractImagesFromPart(
|
||||
images: GeneratedImageAsset[],
|
||||
part: unknown,
|
||||
malformedResponseError?: string,
|
||||
): void {
|
||||
if (!isRecord(part)) {
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
if (part.type === "text") {
|
||||
return;
|
||||
}
|
||||
if (part.type === "image_url") {
|
||||
const imageUrl = part.image_url ?? part.imageUrl;
|
||||
if (!isRecord(imageUrl)) {
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
const url = normalizeOptionalString(imageUrl.url);
|
||||
if (url) {
|
||||
pushDataUrlImage(images, url, malformedResponseError);
|
||||
return;
|
||||
}
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawBase64 = normalizeOptionalString(part.b64_json);
|
||||
if (rawBase64) {
|
||||
const image = generatedImageAssetFromBase64({ base64: rawBase64, index: images.length });
|
||||
if (image) {
|
||||
images.push(image);
|
||||
return;
|
||||
}
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
if ("b64_json" in part) {
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineData = part.inlineData ?? part.inline_data;
|
||||
if (inlineData === undefined || inlineData === null) {
|
||||
return;
|
||||
}
|
||||
if (!isRecord(inlineData)) {
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
const data = normalizeOptionalString(inlineData.data);
|
||||
if (!data) {
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
return;
|
||||
}
|
||||
const mimeType =
|
||||
normalizeOptionalString(inlineData.mimeType) ??
|
||||
normalizeOptionalString(inlineData.mime_type) ??
|
||||
"image/png";
|
||||
const image = generatedImageAssetFromBase64({
|
||||
base64: data,
|
||||
index: images.length,
|
||||
mimeType,
|
||||
});
|
||||
if (image) {
|
||||
images.push(image);
|
||||
return;
|
||||
}
|
||||
throwMalformedOpenRouterImageResponse(malformedResponseError);
|
||||
}
|
||||
|
||||
export function extractOpenRouterImagesFromResponse(
|
||||
body: unknown,
|
||||
options: { malformedResponseError?: string } = {},
|
||||
): GeneratedImageAsset[] {
|
||||
if (!isRecord(body)) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
return [];
|
||||
}
|
||||
const choices = body.choices;
|
||||
if (choices === undefined || choices === null) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(choices)) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
return [];
|
||||
}
|
||||
|
||||
const images: GeneratedImageAsset[] = [];
|
||||
for (const choice of choices) {
|
||||
if (!isRecord(choice)) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
continue;
|
||||
}
|
||||
const message = choice.message;
|
||||
if (message === undefined || message === null) {
|
||||
continue;
|
||||
}
|
||||
if (!isRecord(message)) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
continue;
|
||||
}
|
||||
|
||||
const messageImages = message.images;
|
||||
if (messageImages !== undefined && messageImages !== null) {
|
||||
if (!Array.isArray(messageImages)) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
continue;
|
||||
}
|
||||
for (const entry of messageImages) {
|
||||
if (!isRecord(entry)) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
continue;
|
||||
}
|
||||
const imageUrl = entry.image_url ?? entry.imageUrl;
|
||||
if (!isRecord(imageUrl)) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
continue;
|
||||
}
|
||||
const url = normalizeOptionalString(imageUrl.url);
|
||||
if (!url) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
continue;
|
||||
}
|
||||
pushDataUrlImage(images, url, options.malformedResponseError);
|
||||
}
|
||||
}
|
||||
|
||||
const content = message.content;
|
||||
if (typeof content === "string" && content.length > 0) {
|
||||
const dataUrlPattern = /data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/g;
|
||||
for (const match of content.matchAll(dataUrlPattern)) {
|
||||
pushDataUrlImage(images, match[0]);
|
||||
}
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
extractImagesFromPart(images, part, options.malformedResponseError);
|
||||
}
|
||||
} else if (content !== undefined && content !== null) {
|
||||
throwMalformedOpenRouterImageResponse(options.malformedResponseError);
|
||||
}
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
function resolveImageCount(count: number | undefined): number {
|
||||
if (typeof count !== "number" || !Number.isFinite(count)) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(1, Math.min(MAX_IMAGE_RESULTS, Math.trunc(count)));
|
||||
}
|
||||
|
||||
function resolveGeneratedImageMaxBytes(req: {
|
||||
cfg: { agents?: { defaults?: { mediaMaxMb?: number } } };
|
||||
}): number {
|
||||
const configured = req.cfg.agents?.defaults?.mediaMaxMb;
|
||||
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
||||
return Math.floor(configured * MB);
|
||||
}
|
||||
return MAX_IMAGE_BYTES;
|
||||
}
|
||||
|
||||
function isGeminiImageModel(model: string): boolean {
|
||||
return model.startsWith("google/gemini-");
|
||||
}
|
||||
|
||||
function buildMessageContent(
|
||||
req: ImageGenerationRequest,
|
||||
):
|
||||
| string
|
||||
| Array<{ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }> {
|
||||
const inputImages = req.inputImages ?? [];
|
||||
if (inputImages.length === 0) {
|
||||
return req.prompt;
|
||||
}
|
||||
return [
|
||||
{ type: "text", text: req.prompt },
|
||||
...inputImages.map((image) => ({
|
||||
type: "image_url" as const,
|
||||
image_url: { url: toImageDataUrl(image) },
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
function buildImageConfig(req: ImageGenerationRequest, model: string): Record<string, string> {
|
||||
if (!isGeminiImageModel(model)) {
|
||||
return {};
|
||||
}
|
||||
const imageConfig: Record<string, string> = {};
|
||||
const aspectRatio = normalizeOptionalString(req.aspectRatio);
|
||||
if (aspectRatio) {
|
||||
imageConfig.aspect_ratio = aspectRatio;
|
||||
}
|
||||
const resolution = normalizeOptionalString(req.resolution);
|
||||
if (resolution) {
|
||||
imageConfig.image_size = resolution;
|
||||
}
|
||||
return imageConfig;
|
||||
}
|
||||
|
||||
export function buildOpenRouterImageGenerationProvider(): ImageGenerationProvider {
|
||||
return {
|
||||
id: "openrouter",
|
||||
label: "OpenRouter",
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
models: [...SUPPORTED_MODELS],
|
||||
isConfigured: ({ agentDir }) =>
|
||||
isProviderApiKeyConfigured({ provider: "openrouter", agentDir }),
|
||||
capabilities: {
|
||||
generate: {
|
||||
maxCount: MAX_IMAGE_RESULTS,
|
||||
supportsSize: false,
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
},
|
||||
edit: {
|
||||
enabled: true,
|
||||
maxCount: MAX_IMAGE_RESULTS,
|
||||
maxInputImages: 5,
|
||||
supportsSize: false,
|
||||
supportsAspectRatio: true,
|
||||
supportsResolution: true,
|
||||
},
|
||||
geometry: {
|
||||
aspectRatios: [...SUPPORTED_ASPECT_RATIOS],
|
||||
resolutions: ["1K", "2K", "4K"],
|
||||
},
|
||||
},
|
||||
async generateImage(req) {
|
||||
const auth = await resolveApiKeyForProvider({
|
||||
provider: "openrouter",
|
||||
cfg: req.cfg,
|
||||
agentDir: req.agentDir,
|
||||
store: req.authStore,
|
||||
});
|
||||
if (!auth.apiKey) {
|
||||
throw new Error("OpenRouter API key missing");
|
||||
}
|
||||
|
||||
const model = normalizeOptionalString(req.model) ?? DEFAULT_MODEL;
|
||||
const imageConfig = buildImageConfig(req, model);
|
||||
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
|
||||
resolveProviderHttpRequestConfig({
|
||||
baseUrl: req.cfg?.models?.providers?.openrouter?.baseUrl,
|
||||
defaultBaseUrl: OPENROUTER_BASE_URL,
|
||||
allowPrivateNetwork: false,
|
||||
defaultHeaders: {
|
||||
Authorization: `Bearer ${auth.apiKey}`,
|
||||
"HTTP-Referer": "https://openclaw.ai",
|
||||
"X-OpenRouter-Title": "OpenClaw",
|
||||
},
|
||||
provider: "openrouter",
|
||||
capability: "image",
|
||||
transport: "http",
|
||||
});
|
||||
|
||||
const count = resolveImageCount(req.count);
|
||||
const { response, release } = await postJsonRequest({
|
||||
url: `${baseUrl}/chat/completions`,
|
||||
headers,
|
||||
body: {
|
||||
model,
|
||||
messages: [{ role: "user", content: buildMessageContent(req) }],
|
||||
modalities: ["image", "text"],
|
||||
n: count,
|
||||
...(Object.keys(imageConfig).length > 0 ? { image_config: imageConfig } : {}),
|
||||
},
|
||||
timeoutMs: req.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
fetchFn: fetch,
|
||||
allowPrivateNetwork,
|
||||
ssrfPolicy: req.ssrfPolicy,
|
||||
dispatcherPolicy,
|
||||
});
|
||||
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "OpenRouter image generation failed");
|
||||
const payload = await readProviderJsonResponse(response, "openrouter.image-generation", {
|
||||
maxBytes: resolveInlineImageJsonResponseMaxBytes(
|
||||
count,
|
||||
resolveGeneratedImageMaxBytes(req),
|
||||
),
|
||||
});
|
||||
const images = extractOpenRouterImagesFromResponse(payload, {
|
||||
malformedResponseError: OPENROUTER_IMAGE_MALFORMED_RESPONSE,
|
||||
});
|
||||
if (images.length === 0) {
|
||||
throw new Error("OpenRouter image generation response missing image data");
|
||||
}
|
||||
return { images, model };
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user