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/openai.ts
1import path from "node:path";2import { readFile } from "node:fs/promises";3import type { CliArgs } from "../types";45export function getDefaultModel(): string {6return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";7}89type OpenAIImageResponse = { data: Array<{ url?: string; b64_json?: string }> };1011export function parseAspectRatio(ar: string): { width: number; height: number } | null {12const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);13if (!match) return null;14const w = parseFloat(match[1]!);15const h = parseFloat(match[2]!);16if (w <= 0 || h <= 0) return null;17return { width: w, height: h };18}1920type SizeMapping = {21square: string;22landscape: string;23portrait: string;24};2526export function getOpenAISize(27model: string,28ar: string | null,29quality: CliArgs["quality"]30): string {31const isDalle3 = model.includes("dall-e-3");32const isDalle2 = model.includes("dall-e-2");3334if (isDalle2) {35return "1024x1024";36}3738const sizes: SizeMapping = isDalle339? {40square: "1024x1024",41landscape: "1792x1024",42portrait: "1024x1792",43}44: {45square: "1024x1024",46landscape: "1536x1024",47portrait: "1024x1536",48};4950if (!ar) return sizes.square;5152const parsed = parseAspectRatio(ar);53if (!parsed) return sizes.square;5455const ratio = parsed.width / parsed.height;5657if (Math.abs(ratio - 1) < 0.1) return sizes.square;58if (ratio > 1.5) return sizes.landscape;59if (ratio < 0.67) return sizes.portrait;60return sizes.square;61}6263export async function generateImage(64prompt: string,65model: string,66args: CliArgs67): Promise<Uint8Array> {68const baseURL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";69const apiKey = process.env.OPENAI_API_KEY;7071if (!apiKey) {72throw new Error(73"OPENAI_API_KEY is required. Codex/ChatGPT desktop login does not automatically grant OpenAI Images API access to this script."74);75}7677if (process.env.OPENAI_IMAGE_USE_CHAT === "true") {78return generateWithChatCompletions(baseURL, apiKey, prompt, model);79}8081const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);8283if (args.referenceImages.length > 0) {84if (model.includes("dall-e-2") || model.includes("dall-e-3")) {85throw new Error(86"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-1.5 (or another gpt-image model)."87);88}89return generateWithOpenAIEdits(baseURL, apiKey, prompt, model, size, args.referenceImages, args.quality);90}9192return generateWithOpenAIGenerations(baseURL, apiKey, prompt, model, size, args.quality);93}9495async function generateWithChatCompletions(96baseURL: string,97apiKey: string,98prompt: string,99model: string100): Promise<Uint8Array> {101const res = await fetch(`${baseURL}/chat/completions`, {102method: "POST",103headers: {104"Content-Type": "application/json",105Authorization: `Bearer ${apiKey}`,106},107body: JSON.stringify({108model,109messages: [{ role: "user", content: prompt }],110}),111});112113if (!res.ok) {114const err = await res.text();115throw new Error(`OpenAI API error: ${err}`);116}117118const result = (await res.json()) as { choices: Array<{ message: { content: string } }> };119const content = result.choices[0]?.message?.content ?? "";120121const match = content.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)/);122if (match) {123return Uint8Array.from(Buffer.from(match[1]!, "base64"));124}125126throw new Error("No image found in chat completions response");127}128129async function generateWithOpenAIGenerations(130baseURL: string,131apiKey: string,132prompt: string,133model: string,134size: string,135quality: CliArgs["quality"]136): Promise<Uint8Array> {137const body: Record<string, any> = { model, prompt, size };138139if (model.includes("dall-e-3")) {140body.quality = quality === "2k" ? "hd" : "standard";141}142143const res = await fetch(`${baseURL}/images/generations`, {144method: "POST",145headers: {146"Content-Type": "application/json",147Authorization: `Bearer ${apiKey}`,148},149body: JSON.stringify(body),150});151152if (!res.ok) {153const err = await res.text();154throw new Error(`OpenAI API error: ${err}`);155}156157const result = (await res.json()) as OpenAIImageResponse;158return extractImageFromResponse(result);159}160161async function generateWithOpenAIEdits(162baseURL: string,163apiKey: string,164prompt: string,165model: string,166size: string,167referenceImages: string[],168quality: CliArgs["quality"]169): Promise<Uint8Array> {170const form = new FormData();171form.append("model", model);172form.append("prompt", prompt);173form.append("size", size);174175if (model.includes("gpt-image")) {176form.append("quality", quality === "2k" ? "high" : "medium");177}178179for (const refPath of referenceImages) {180const bytes = await readFile(refPath);181const filename = path.basename(refPath);182const mimeType = getMimeType(filename);183const blob = new Blob([bytes], { type: mimeType });184form.append("image[]", blob, filename);185}186187const res = await fetch(`${baseURL}/images/edits`, {188method: "POST",189headers: {190Authorization: `Bearer ${apiKey}`,191},192body: form,193});194195if (!res.ok) {196const err = await res.text();197throw new Error(`OpenAI edits API error: ${err}`);198}199200const result = (await res.json()) as OpenAIImageResponse;201return extractImageFromResponse(result);202}203204export function getMimeType(filename: string): string {205const ext = path.extname(filename).toLowerCase();206if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";207if (ext === ".webp") return "image/webp";208if (ext === ".gif") return "image/gif";209return "image/png";210}211212export async function extractImageFromResponse(result: OpenAIImageResponse): Promise<Uint8Array> {213const img = result.data[0];214215if (img?.b64_json) {216return Uint8Array.from(Buffer.from(img.b64_json, "base64"));217}218219if (img?.url) {220const imgRes = await fetch(img.url);221if (!imgRes.ok) throw new Error("Failed to download image");222const buf = await imgRes.arrayBuffer();223return new Uint8Array(buf);224}225226throw new Error("No image in response");227}228