Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate images via OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream, and Replicate APIs with batch support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/providers/openrouter.ts
1import path from "node:path";2import { readFile } from "node:fs/promises";3import type { CliArgs } from "../types";45const DEFAULT_MODEL = "google/gemini-3.1-flash-image";6const COMMON_ASPECT_RATIOS = [7"1:1",8"2:3",9"3:2",10"3:4",11"4:3",12"4:5",13"5:4",14"9:16",15"16:9",16"21:9",17];18const GEMINI_EXTENDED_ASPECT_RATIOS = ["1:4", "4:1", "1:8", "8:1"];1920type OpenRouterImageEntry = {21image_url?: string | { url?: string | null } | null;22imageUrl?: string | { url?: string | null } | null;23};2425type OpenRouterMessagePart = {26type?: string;27text?: string;28image_url?: string | { url?: string | null } | null;29imageUrl?: string | { url?: string | null } | null;30};3132type OpenRouterResponse = {33choices?: Array<{34finish_reason?: string | null;35native_finish_reason?: string | null;36message?: {37images?: OpenRouterImageEntry[];38content?: string | OpenRouterMessagePart[] | null;39};40}>;41};4243export function getDefaultModel(): string {44return process.env.OPENROUTER_IMAGE_MODEL || DEFAULT_MODEL;45}4647function normalizeModelId(model: string): string {48return model.trim().toLowerCase().split(":")[0]!;49}5051function isTextAndImageModel(model: string): boolean {52const normalized = normalizeModelId(model);53if (normalized === "openrouter/auto") {54return true;55}5657if (normalized.startsWith("google/gemini-") && normalized.includes("image")) {58return true;59}6061if (normalized.startsWith("openai/gpt-") && normalized.includes("image")) {62return true;63}6465return false;66}6768function getSupportedAspectRatios(model: string): Set<string> {69const normalized = normalizeModelId(model);70if (71normalized !== "google/gemini-3.1-flash-image" &&72normalized !== "google/gemini-3.1-flash-image-preview"73) {74return new Set(COMMON_ASPECT_RATIOS);75}7677return new Set([...COMMON_ASPECT_RATIOS, ...GEMINI_EXTENDED_ASPECT_RATIOS]);78}7980function getApiKey(): string | null {81return process.env.OPENROUTER_API_KEY || null;82}8384function getBaseUrl(): string {85const base = process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";86return base.replace(/\/+$/g, "");87}8889function getHeaders(apiKey: string): Record<string, string> {90const headers: Record<string, string> = {91"Content-Type": "application/json",92Authorization: `Bearer ${apiKey}`,93};9495const referer = process.env.OPENROUTER_HTTP_REFERER?.trim();96if (referer) {97headers["HTTP-Referer"] = referer;98}99100const title = process.env.OPENROUTER_TITLE?.trim();101if (title) {102headers["X-OpenRouter-Title"] = title;103headers["X-Title"] = title;104}105106return headers;107}108109function parsePixelSize(value: string): { width: number; height: number } | null {110const match = value.match(/^(\d+)\s*[xX]\s*(\d+)$/);111if (!match) return null;112113const width = parseInt(match[1]!, 10);114const height = parseInt(match[2]!, 10);115116if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {117return null;118}119120return { width, height };121}122123function gcd(a: number, b: number): number {124let x = Math.abs(a);125let y = Math.abs(b);126while (y !== 0) {127const next = x % y;128x = y;129y = next;130}131return x || 1;132}133134function inferAspectRatio(size: string | null): string | null {135if (!size) return null;136const parsed = parsePixelSize(size);137if (!parsed) return null;138139const divisor = gcd(parsed.width, parsed.height);140return `${parsed.width / divisor}:${parsed.height / divisor}`;141}142143function inferImageSize(size: string | null): "1K" | "2K" | "4K" | null {144if (!size) return null;145const parsed = parsePixelSize(size);146if (!parsed) return null;147148const longestEdge = Math.max(parsed.width, parsed.height);149if (longestEdge <= 1024) return "1K";150if (longestEdge <= 2048) return "2K";151return "4K";152}153154export function getImageSize(args: CliArgs): "1K" | "2K" | "4K" | null {155if (args.imageSize) return args.imageSize as "1K" | "2K" | "4K";156157const inferredFromSize = inferImageSize(args.size);158if (inferredFromSize) return inferredFromSize;159160if (args.quality === "normal") return "1K";161if (args.quality === "2k") return "2K";162return null;163}164165export function getAspectRatio(model: string, args: CliArgs): string | null {166if (args.aspectRatio) return args.aspectRatio;167168const inferred = inferAspectRatio(args.size);169if (!inferred || !getSupportedAspectRatios(model).has(inferred)) {170return null;171}172173return inferred;174}175176function getModalities(model: string): string[] {177return isTextAndImageModel(model) ? ["image", "text"] : ["image"];178}179180export function validateArgs(model: string, args: CliArgs): void {181const requestedAspectRatio = args.aspectRatio || inferAspectRatio(args.size);182if (!requestedAspectRatio) {183return;184}185186const supported = getSupportedAspectRatios(model);187if (supported.has(requestedAspectRatio)) {188return;189}190191const requestedValue = args.aspectRatio192? `aspect ratio ${requestedAspectRatio}`193: `size ${args.size} (aspect ratio ${requestedAspectRatio})`;194195throw new Error(196`OpenRouter model ${model} does not support ${requestedValue}. Supported values: ${Array.from(supported).join(", ")}`197);198}199200function getMimeType(filename: string): string {201const ext = path.extname(filename).toLowerCase();202if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";203if (ext === ".webp") return "image/webp";204if (ext === ".gif") return "image/gif";205return "image/png";206}207208async function readImageAsDataUrl(filePath: string): Promise<string> {209const bytes = await readFile(filePath);210return `data:${getMimeType(filePath)};base64,${bytes.toString("base64")}`;211}212213export function buildContent(214prompt: string,215referenceImages: string[],216): string | Array<Record<string, unknown>> {217if (referenceImages.length === 0) {218return prompt;219}220221const content: Array<Record<string, unknown>> = [{ type: "text", text: prompt }];222223for (const imageUrl of referenceImages) {224content.push({225type: "image_url",226image_url: { url: imageUrl },227});228}229230return content;231}232233function extractImageUrl(entry: OpenRouterImageEntry | OpenRouterMessagePart): string | null {234const value = entry.image_url ?? entry.imageUrl;235if (!value) return null;236if (typeof value === "string") return value;237return value.url ?? null;238}239240function decodeDataUrl(value: string): Uint8Array | null {241const match = value.match(/^data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)$/);242if (!match) return null;243return Uint8Array.from(Buffer.from(match[1]!, "base64"));244}245246async function downloadImage(value: string): Promise<Uint8Array> {247const inline = decodeDataUrl(value);248if (inline) return inline;249250if (value.startsWith("http://") || value.startsWith("https://")) {251const response = await fetch(value);252if (!response.ok) {253throw new Error(`Failed to download OpenRouter image: ${response.status}`);254}255const buffer = await response.arrayBuffer();256return new Uint8Array(buffer);257}258259return Uint8Array.from(Buffer.from(value, "base64"));260}261262export async function extractImageFromResponse(result: OpenRouterResponse): Promise<Uint8Array> {263const choice = result.choices?.[0];264const message = choice?.message;265266for (const image of message?.images ?? []) {267const imageUrl = extractImageUrl(image);268if (imageUrl) return downloadImage(imageUrl);269}270271if (Array.isArray(message?.content)) {272for (const item of message.content) {273const imageUrl = extractImageUrl(item);274if (imageUrl) return downloadImage(imageUrl);275276if (item.type === "text" && item.text) {277const inline = decodeDataUrl(item.text);278if (inline) return inline;279}280}281} else if (typeof message?.content === "string") {282const inline = decodeDataUrl(message.content);283if (inline) return inline;284}285286const finishReason =287choice?.native_finish_reason || choice?.finish_reason || "unknown";288throw new Error(289`No image in OpenRouter response (finish_reason=${finishReason})`,290);291}292293export function buildRequestBody(294prompt: string,295model: string,296args: CliArgs,297referenceImages: string[],298): Record<string, unknown> {299validateArgs(model, args);300301const imageConfig: Record<string, string> = {};302303const imageSize = getImageSize(args);304if (imageSize) {305imageConfig.image_size = imageSize;306}307308const aspectRatio = getAspectRatio(model, args);309if (aspectRatio) {310imageConfig.aspect_ratio = aspectRatio;311}312313const body: Record<string, unknown> = {314messages: [315{316role: "user",317content: buildContent(prompt, referenceImages),318},319],320modalities: getModalities(model),321stream: false,322};323324if (Object.keys(imageConfig).length > 0) {325body.image_config = imageConfig;326body.provider = {327require_parameters: true,328};329}330331return body;332}333334export async function generateImage(335prompt: string,336model: string,337args: CliArgs338): Promise<Uint8Array> {339const apiKey = getApiKey();340if (!apiKey) {341throw new Error("OPENROUTER_API_KEY is required. Get one at https://openrouter.ai/settings/keys");342}343344const referenceImages: string[] = [];345for (const refPath of args.referenceImages) {346referenceImages.push(await readImageAsDataUrl(refPath));347}348349const body = {350model,351...buildRequestBody(prompt, model, args, referenceImages),352};353354console.log(355`Generating image with OpenRouter (${model})...`,356(body.image_config as Record<string, string>),357);358359const response = await fetch(`${getBaseUrl()}/chat/completions`, {360method: "POST",361headers: getHeaders(apiKey),362body: JSON.stringify(body),363});364365if (!response.ok) {366const errorText = await response.text();367throw new Error(`OpenRouter API error (${response.status}): ${errorText}`);368}369370const result = (await response.json()) as OpenRouterResponse;371return extractImageFromResponse(result);372}373