From 63c61aac6f36815651a54fc01cce33f5042ba667 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Mon, 4 May 2026 17:07:02 +0200 Subject: [PATCH] feat: image models --- packages/ai/README.md | 5 +- packages/ai/package.json | 3 +- packages/ai/scripts/generate-image-models.ts | 131 +++++++++ packages/ai/src/image-models.generated.ts | 264 +++++++++++++++++++ packages/ai/src/image-models.ts | 42 +++ packages/ai/src/index.ts | 1 + 6 files changed, 443 insertions(+), 3 deletions(-) create mode 100644 packages/ai/scripts/generate-image-models.ts create mode 100644 packages/ai/src/image-models.generated.ts create mode 100644 packages/ai/src/image-models.ts diff --git a/packages/ai/README.md b/packages/ai/README.md index 18d3b945..8d09c8ef 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -1262,10 +1262,11 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - Add credential detection in `env-api-keys.ts` for the new provider - Ensure `streamSimple` handles auth lookup via `getEnvApiKey()` or provider-specific auth -#### 4. Model Generation (`scripts/generate-models.ts`) +#### 4. Model Generation (`scripts/generate-models.ts`, `scripts/generate-image-models.ts`) - Add logic to fetch and parse models from the provider's source (e.g., models.dev API) -- Map provider model data to the standardized `Model` interface +- Map chat/tool-capable provider model data to the standardized `Model` interface via `scripts/generate-models.ts` +- Map image-generation provider model data to the standardized `ImagesModel` interface via `scripts/generate-image-models.ts` - Handle provider-specific quirks (pricing format, capability flags, model ID transformations) #### 5. Tests (`test/`) diff --git a/packages/ai/package.json b/packages/ai/package.json index 51a70afa..e2cd9357 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -65,7 +65,8 @@ "scripts": { "clean": "shx rm -rf dist", "generate-models": "npx tsx scripts/generate-models.ts", - "build": "npm run generate-models && tsgo -p tsconfig.build.json", + "generate-image-models": "npx tsx scripts/generate-image-models.ts", + "build": "npm run generate-models && npm run generate-image-models && tsgo -p tsconfig.build.json", "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "dev:tsc": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "test": "vitest --run", diff --git a/packages/ai/scripts/generate-image-models.ts b/packages/ai/scripts/generate-image-models.ts new file mode 100644 index 00000000..bd0c6727 --- /dev/null +++ b/packages/ai/scripts/generate-image-models.ts @@ -0,0 +1,131 @@ +#!/usr/bin/env tsx + +import { writeFileSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import type { ImagesModel } from "../src/types.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageRoot = join(__dirname, ".."); +const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"; + +interface OpenRouterModelRecord { + id: string; + name: string; + context_length?: number; + architecture?: { + input_modalities?: string[]; + output_modalities?: string[]; + }; + pricing?: { + prompt?: string; + completion?: string; + input_cache_read?: string; + input_cache_write?: string; + }; +} + +async function fetchOpenRouterImageModels(): Promise[]> { + try { + console.log("Fetching image models from OpenRouter API..."); + const response = await fetch(`${OPENROUTER_BASE_URL}/models?output_modalities=image`); + const data = (await response.json()) as { data?: OpenRouterModelRecord[] }; + const models: ImagesModel<"openrouter-images">[] = []; + + for (const model of data.data ?? []) { + const input = Array.from( + new Set( + (model.architecture?.input_modalities ?? []) + .filter((modality): modality is "text" | "image" => modality === "text" || modality === "image"), + ), + ); + const output = Array.from( + new Set( + (model.architecture?.output_modalities ?? []).filter( + (modality): modality is "text" | "image" => modality === "text" || modality === "image", + ), + ), + ); + + if (!output.includes("image")) continue; + if (input.length === 0) input.push("text"); + + models.push({ + id: model.id, + name: model.name, + api: "openrouter-images", + provider: "openrouter", + baseUrl: OPENROUTER_BASE_URL, + input, + output, + cost: { + input: parseFloat(model.pricing?.prompt || "0") * 1_000_000, + output: parseFloat(model.pricing?.completion || "0") * 1_000_000, + cacheRead: parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000, + cacheWrite: parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000, + }, + }); + } + + console.log(`Fetched ${models.length} image models from OpenRouter`); + return models; + } catch (error) { + console.error("Failed to fetch OpenRouter image models:", error); + return []; + } +} + +function generateImageModelsFile(models: ImagesModel<"openrouter-images">[]): string { + const imageModelsByProvider = { + openrouter: Object.fromEntries( + models + .sort((a, b) => a.id.localeCompare(b.id)) + .map((model) => [ + model.id, + `{ + id: ${JSON.stringify(model.id)}, + name: ${JSON.stringify(model.name)}, + api: ${JSON.stringify(model.api)}, + provider: ${JSON.stringify(model.provider)}, + baseUrl: ${JSON.stringify(model.baseUrl)}, + input: ${JSON.stringify(model.input)}, + output: ${JSON.stringify(model.output)}, + cost: ${JSON.stringify(model.cost, null, 2).replace(/^/gm, "\t")} + } satisfies ImagesModel<${JSON.stringify(model.api)}>`, + ]), + ), + }; + + const providerEntries = Object.entries(imageModelsByProvider) + .map(([provider, providerModels]) => { + const modelEntries = Object.entries(providerModels) + .map(([id, serialized]) => `\t\t${JSON.stringify(id)}: ${serialized},`) + .join("\n"); + return `\t${JSON.stringify(provider)}: {\n${modelEntries}\n\t},`; + }) + .join("\n"); + + return `// This file is auto-generated by scripts/generate-image-models.ts +// Do not edit manually - run 'npm run generate-image-models' to update + +import type { ImagesApi, ImagesModel } from "./types.js"; + +export const IMAGE_MODELS = { +${providerEntries} +} as const satisfies Record>>; +`; +} + +async function main(): Promise { + const models = await fetchOpenRouterImageModels(); + const output = generateImageModelsFile(models); + const outputPath = join(packageRoot, "src", "image-models.generated.ts"); + writeFileSync(outputPath, output, "utf-8"); + console.log(`Generated ${outputPath}`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts new file mode 100644 index 00000000..9bbee4c8 --- /dev/null +++ b/packages/ai/src/image-models.generated.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by scripts/generate-image-models.ts +// Do not edit manually - run 'npm run generate-image-models' to update + +import type { ImagesApi, ImagesModel } from "./types.js"; + +export const IMAGE_MODELS = { + openrouter: { + "black-forest-labs/flux.2-flex": { + id: "black-forest-labs/flux.2-flex", + name: "Black Forest Labs: FLUX.2 Flex", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "black-forest-labs/flux.2-klein-4b": { + id: "black-forest-labs/flux.2-klein-4b", + name: "Black Forest Labs: FLUX.2 Klein 4B", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "black-forest-labs/flux.2-max": { + id: "black-forest-labs/flux.2-max", + name: "Black Forest Labs: FLUX.2 Max", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "black-forest-labs/flux.2-pro": { + id: "black-forest-labs/flux.2-pro", + name: "Black Forest Labs: FLUX.2 Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "bytedance-seed/seedream-4.5": { + id: "bytedance-seed/seedream-4.5", + name: "ByteDance Seed: Seedream 4.5", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "google/gemini-2.5-flash-image": { + id: "google/gemini-2.5-flash-image", + name: "Google: Nano Banana (Gemini 2.5 Flash Image)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 0.3, + output: 2.5, + cacheRead: 0.03, + cacheWrite: 0.08333333333333334, + }, + } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3-pro-image-preview": { + id: "google/gemini-3-pro-image-preview", + name: "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0.375, + }, + } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3.1-flash-image-preview": { + id: "google/gemini-3.1-flash-image-preview", + name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image Preview)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "openai/gpt-5-image": { + id: "openai/gpt-5-image", + name: "OpenAI: GPT-5 Image", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 10, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "openai/gpt-5-image-mini": { + id: "openai/gpt-5-image-mini", + name: "OpenAI: GPT-5 Image Mini", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 2.5, + output: 2, + cacheRead: 0.25, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "openai/gpt-5.4-image-2": { + id: "openai/gpt-5.4-image-2", + name: "OpenAI: GPT-5.4 Image 2", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 8, + output: 15, + cacheRead: 2, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "openrouter/auto": { + id: "openrouter/auto", + name: "Auto Router", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["text", "image"], + cost: { + input: -1000000, + output: -1000000, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "sourceful/riverflow-v2-fast": { + id: "sourceful/riverflow-v2-fast", + name: "Sourceful: Riverflow V2 Fast", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "sourceful/riverflow-v2-fast-preview": { + id: "sourceful/riverflow-v2-fast-preview", + name: "Sourceful: Riverflow V2 Fast Preview", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "sourceful/riverflow-v2-max-preview": { + id: "sourceful/riverflow-v2-max-preview", + name: "Sourceful: Riverflow V2 Max Preview", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "sourceful/riverflow-v2-pro": { + id: "sourceful/riverflow-v2-pro", + name: "Sourceful: Riverflow V2 Pro", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + "sourceful/riverflow-v2-standard-preview": { + id: "sourceful/riverflow-v2-standard-preview", + name: "Sourceful: Riverflow V2 Standard Preview", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["text", "image"], + output: ["image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, + }, +} as const satisfies Record>>; diff --git a/packages/ai/src/image-models.ts b/packages/ai/src/image-models.ts new file mode 100644 index 00000000..afa4f415 --- /dev/null +++ b/packages/ai/src/image-models.ts @@ -0,0 +1,42 @@ +import { IMAGE_MODELS } from "./image-models.generated.js"; +import type { ImagesApi, ImagesModel, KnownImagesProvider } from "./types.js"; + +const imageModelRegistry: Map>> = new Map(); + +for (const [provider, models] of Object.entries(IMAGE_MODELS)) { + const providerModels = new Map>(); + for (const [id, model] of Object.entries(models)) { + providerModels.set(id, model as ImagesModel); + } + imageModelRegistry.set(provider, providerModels); +} + +type ImageModelApi< + TProvider extends KnownImagesProvider, + TModelId extends keyof (typeof IMAGE_MODELS)[TProvider], +> = (typeof IMAGE_MODELS)[TProvider][TModelId] extends { api: infer TApi } + ? TApi extends ImagesApi + ? TApi + : never + : never; + +export function getImageModel< + TProvider extends KnownImagesProvider, + TModelId extends keyof (typeof IMAGE_MODELS)[TProvider], +>(provider: TProvider, modelId: TModelId): ImagesModel> { + const providerModels = imageModelRegistry.get(provider); + return providerModels?.get(modelId as string) as ImagesModel>; +} + +export function getImageProviders(): KnownImagesProvider[] { + return Array.from(imageModelRegistry.keys()) as KnownImagesProvider[]; +} + +export function getImageModels( + provider: TProvider, +): ImagesModel>[] { + const models = imageModelRegistry.get(provider); + return models + ? (Array.from(models.values()) as ImagesModel>[]) + : []; +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index d3c808e9..48670068 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -3,6 +3,7 @@ export { Type } from "typebox"; export * from "./api-registry.js"; export * from "./env-api-keys.js"; +export * from "./image-models.js"; export * from "./images.js"; export * from "./images-api-registry.js"; export * from "./models.js";