Initial release: MCP Image Generator

Multi-provider MCP server for AI image generation.
Supports OpenRouter, Together AI, Replicate, and fal.ai.
Works with Claude Code, Cursor, Claude Desktop, OpenCode, and Charm.
Install via: npx -y mcp-image-generator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fdciabdul
2026-03-24 21:15:54 +07:00
commit 510a722f74
15 changed files with 2322 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(npm run:*)",
"Bash(chmod:*)",
"Bash(git init:*)",
"Bash(git checkout:*)",
"Bash(git add:*)"
]
}
}

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
dist/
*.js.map
.env
generated-images/

11
.mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"image-gen": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}"
}
}
}
}

205
README.md Normal file
View File

@@ -0,0 +1,205 @@
<p align="center">
<img src="logo.png" width="200" alt="MCP Image Generator Logo">
</p>
<h1 align="center">MCP Image Generator</h1>
<p align="center">
MCP server for AI image generation. Works with <b>Claude Code</b>, <b>Cursor</b>, <b>Claude Desktop</b>, <b>OpenCode</b>, <b>Charm (Crush/Mods)</b>, and any MCP-compatible AI agent.
</p>
**Providers supported** (image models only):
- **OpenRouter** (default) — Gemini Flash Image, FLUX 2, Sourceful Riverflow, GPT-5 Image
- **Together AI** — FLUX.1 Schnell/Dev/Pro, Stable Diffusion XL
- **Replicate** — FLUX Schnell, FLUX 1.1 Pro, SDXL, Ideogram
- **fal.ai** — FLUX Dev/Schnell/2 Pro, Recraft V3
## Quick Start
No install needed — just use `npx`:
```bash
OPENROUTER_API_KEY="sk-or-v1-..." npx -y mcp-image-generator
```
## Tools
### `generate_image`
Generate an image from a text prompt.
| Parameter | Type | Description |
|-----------|------|-------------|
| `prompt` | string (required) | Text description of the image |
| `model` | string | Model ID (e.g. `google/gemini-2.5-flash-image`) |
| `provider` | string | `openrouter` (default), `together`, `replicate`, `fal` |
| `aspect_ratio` | string | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3` |
| `image_size` | string | `1K` (default), `2K`, `4K` |
| `negative_prompt` | string | What to exclude (not all providers support this) |
| `seed` | number | Random seed for reproducibility |
| `save_to` | string | Custom file path to save the image |
### `list_providers`
List all configured providers and their available image models.
| Parameter | Type | Description |
|-----------|------|-------------|
| `provider` | string | Filter by provider name |
---
## Setup for AI Agents / IDEs
### Claude Code
```bash
claude mcp add --scope user --transport stdio image-gen \
--env OPENROUTER_API_KEY=sk-or-v1-xxx \
-- npx -y mcp-image-generator
```
Or create `.mcp.json` in your project root:
```json
{
"mcpServers": {
"image-gen": {
"command": "npx",
"args": ["-y", "mcp-image-generator"],
"env": {
"OPENROUTER_API_KEY": "sk-or-v1-..."
}
}
}
}
```
### Cursor
Edit `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"image-gen": {
"command": "npx",
"args": ["-y", "mcp-image-generator"],
"env": {
"OPENROUTER_API_KEY": "sk-or-v1-..."
}
}
}
}
```
### Claude Desktop
Edit the config file:
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
- **Linux:** `~/.config/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"image-gen": {
"command": "npx",
"args": ["-y", "mcp-image-generator"],
"env": {
"OPENROUTER_API_KEY": "sk-or-v1-..."
}
}
}
}
```
### OpenCode (by SST)
Edit `~/.config/opencode/opencode.json` or `opencode.json` in project root:
```json
{
"mcp": {
"image-gen": {
"type": "local",
"command": ["npx", "-y", "mcp-image-generator"],
"environment": {
"OPENROUTER_API_KEY": "sk-or-v1-..."
},
"enabled": true,
"timeout": 120000
}
}
}
```
### Charm Crush
Add to `crush.json` in your project root:
```json
{
"mcp": {
"image-gen": {
"type": "stdio",
"command": "npx",
"args": ["-y", "mcp-image-generator"],
"env": {
"OPENROUTER_API_KEY": "sk-or-v1-..."
}
}
}
}
```
### Charm Mods
Edit `mods.yml` (`mods --settings`):
```yaml
mcp-servers:
image-gen:
command: [npx]
args:
- -y
- mcp-image-generator
env:
- OPENROUTER_API_KEY=sk-or-v1-...
```
### Any MCP-compatible agent
This server uses **stdio transport** (JSON-RPC over stdin/stdout). Any agent that supports MCP stdio can use it:
```bash
npx -y mcp-image-generator
# Required environment variables (at least one)
OPENROUTER_API_KEY=... # OpenRouter (default)
TOGETHER_API_KEY=... # Together AI
REPLICATE_API_TOKEN=... # Replicate
FAL_KEY=... # fal.ai
```
## Using Multiple Providers
Set multiple API keys to use different providers:
```bash
export OPENROUTER_API_KEY="sk-or-v1-..."
export TOGETHER_API_KEY="..."
export FAL_KEY="..."
```
Then specify the provider when generating:
```
generate_image(prompt="a cat", provider="together", model="black-forest-labs/FLUX.1-schnell")
```
## Environment Variables
| Variable | Provider | Required |
|----------|----------|----------|
| `OPENROUTER_API_KEY` | OpenRouter | At least one |
| `TOGETHER_API_KEY` | Together AI | provider key |
| `REPLICATE_API_TOKEN` | Replicate | is required |
| `FAL_KEY` | fal.ai | |
| `IMAGE_OUTPUT_DIR` | — | Optional. Default: `~/generated-images/` |
## License
MIT

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

1175
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "mcp-image-generator",
"version": "1.0.0",
"description": "MCP server for AI image generation via OpenRouter, Together AI, Replicate, and fal.ai",
"type": "module",
"main": "dist/index.js",
"bin": {
"mcp-image-generator": "dist/index.js"
},
"files": [
"dist",
"README.md",
"logo.png"
],
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"prepublishOnly": "npm run build"
},
"keywords": [
"mcp",
"image-generation",
"openrouter",
"together-ai",
"replicate",
"fal-ai",
"ai",
"claude-code",
"cursor",
"claude-desktop",
"opencode",
"model-context-protocol"
],
"author": "fdciabdul",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.imtaqin.id/fdciabdul/MCP-IMAGE-GENERATOR.git"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^25.5.0",
"typescript": "^6.0.2"
}
}

245
src/index.ts Normal file
View File

@@ -0,0 +1,245 @@
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { createProviders, getDefaultProvider } from "./providers/index.js";
import type { ImageProvider, ImageModel } from "./types.js";
import { writeFile, mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import { existsSync } from "node:fs";
const providers = createProviders();
if (providers.size === 0) {
console.error(
"No API keys configured. Set at least one of: OPENROUTER_API_KEY, TOGETHER_API_KEY, REPLICATE_API_TOKEN, FAL_KEY"
);
process.exit(1);
}
const server = new McpServer({
name: "mcp-image-generator",
version: "1.0.0",
});
// ─── Tool: list_providers ───────────────────────────────────────────────────
server.registerTool("list_providers", {
title: "List Image Providers",
description: "List all configured image generation providers and their available models",
inputSchema: {
provider: z
.string()
.optional()
.describe("Filter by provider name: openrouter, together, replicate, fal"),
},
}, async ({ provider: providerName }) => {
const targetProviders: ImageProvider[] = [];
if (providerName) {
const p = providers.get(providerName);
if (!p) {
return {
content: [
{
type: "text" as const,
text: `Provider "${providerName}" is not configured. Available: ${[...providers.keys()].join(", ")}`,
},
],
};
}
targetProviders.push(p);
} else {
targetProviders.push(...providers.values());
}
const allModels: ImageModel[] = [];
for (const p of targetProviders) {
try {
const models = await p.listModels();
allModels.push(...models);
} catch (err) {
allModels.push({
id: `error-${p.name}`,
name: `Error loading ${p.name} models`,
provider: p.name,
description: String(err),
inputModalities: [],
outputModalities: [],
modalities: [],
});
}
}
const grouped = new Map<string, ImageModel[]>();
for (const m of allModels) {
const list = grouped.get(m.provider) ?? [];
list.push(m);
grouped.set(m.provider, list);
}
let text = `# Available Image Generation Models\n\n`;
text += `**Configured providers:** ${[...providers.keys()].join(", ")}\n\n`;
for (const [provName, models] of grouped) {
text += `## ${provName}\n\n`;
for (const m of models) {
text += `- **${m.id}** — ${m.name}`;
if (m.description) text += `${m.description}`;
if (m.pricing) text += ` (${m.pricing})`;
text += `\n`;
}
text += `\n`;
}
return { content: [{ type: "text" as const, text }] };
});
// ─── Tool: generate_image ───────────────────────────────────────────────────
const outputDir = resolve(process.env["IMAGE_OUTPUT_DIR"] ?? process.env["HOME"] ?? ".", "generated-images");
server.registerTool("generate_image", {
title: "Generate Image",
description:
"Generate an image from a text prompt. Uses OpenRouter by default. Supports multiple providers: openrouter, together, replicate, fal.",
inputSchema: {
prompt: z.string().describe("Text description of the image to generate"),
model: z
.string()
.optional()
.describe("Model ID (e.g. 'google/gemini-2.5-flash-image', 'black-forest-labs/flux-schnell')"),
provider: z
.string()
.optional()
.describe("Provider to use: openrouter (default), together, replicate, fal"),
aspect_ratio: z
.string()
.optional()
.describe("Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3"),
image_size: z
.string()
.optional()
.describe("Image resolution: 1K (default), 2K, 4K"),
negative_prompt: z
.string()
.optional()
.describe("What to exclude from the image (not supported by all providers)"),
seed: z.number().optional().describe("Random seed for reproducibility"),
save_to: z
.string()
.optional()
.describe("Custom file path to save the image. Defaults to ~/generated-images/"),
},
}, async ({ prompt, model, provider: providerName, aspect_ratio, image_size, negative_prompt, seed, save_to }) => {
// Select provider
let selectedProvider: ImageProvider | undefined;
if (providerName) {
selectedProvider = providers.get(providerName);
if (!selectedProvider) {
return {
content: [
{
type: "text" as const,
text: `Provider "${providerName}" is not configured. Available: ${[...providers.keys()].join(", ")}`,
},
],
};
}
} else {
selectedProvider = getDefaultProvider(providers);
}
if (!selectedProvider) {
return {
content: [{ type: "text" as const, text: "No providers configured." }],
};
}
try {
const result = await selectedProvider.generateImage({
prompt,
model,
provider: selectedProvider.name,
aspectRatio: aspect_ratio,
imageSize: image_size,
negativePrompt: negative_prompt,
seed,
});
if (result.images.length === 0) {
return {
content: [
{
type: "text" as const,
text: `Image generation completed but no images were returned. Model: ${result.model}, Provider: ${result.provider}`,
},
],
};
}
// Save images to disk
const saveDir = save_to ? resolve(save_to, "..") : outputDir;
if (!existsSync(saveDir)) {
await mkdir(saveDir, { recursive: true });
}
const contentItems: Array<
| { type: "text"; text: string }
| { type: "image"; data: string; mimeType: string }
> = [];
for (let i = 0; i < result.images.length; i++) {
const img = result.images[i]!;
const ext = img.mimeType.includes("webp")
? "webp"
: img.mimeType.includes("jpeg") || img.mimeType.includes("jpg")
? "jpg"
: "png";
const filename = save_to
? save_to
: join(saveDir, `img_${Date.now()}_${i}.${ext}`);
await writeFile(filename, Buffer.from(img.base64, "base64"));
contentItems.push({
type: "text" as const,
text: `Image ${i + 1} saved to: ${filename}\nModel: ${result.model} | Provider: ${result.provider}${
result.usage?.cost != null ? ` | Cost: $${result.usage.cost.toFixed(4)}` : ""
}`,
});
contentItems.push({
type: "image" as const,
data: img.base64,
mimeType: img.mimeType,
});
}
return { content: contentItems };
} catch (err) {
return {
content: [
{
type: "text" as const,
text: `Image generation failed: ${err instanceof Error ? err.message : String(err)}`,
},
],
};
}
});
// ─── Start Server ───────────────────────────────────────────────────────────
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Image Generator server started");
console.error(`Providers: ${[...providers.keys()].join(", ")}`);
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

124
src/providers/fal.ts Normal file
View File

@@ -0,0 +1,124 @@
import type { ImageProvider, ImageModel, GenerateImageRequest, GenerateImageResult } from "../types.js";
const API_BASE = "https://queue.fal.run";
const FAL_IMAGE_MODELS: ImageModel[] = [
{
id: "fal-ai/flux/dev",
name: "FLUX.1 Dev",
provider: "fal",
description: "FLUX.1 development model on fal.ai",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "fal-ai/flux/schnell",
name: "FLUX.1 Schnell",
provider: "fal",
description: "Fast FLUX model on fal.ai",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "fal-ai/flux-2-pro",
name: "FLUX 2 Pro",
provider: "fal",
description: "Professional FLUX 2 model",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "fal-ai/recraft-v3",
name: "Recraft V3",
provider: "fal",
description: "High quality image generation",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
];
export class FalProvider implements ImageProvider {
name = "fal";
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async listModels(): Promise<ImageModel[]> {
return FAL_IMAGE_MODELS;
}
private parseImageSize(aspectRatio?: string): string {
const sizeMap: Record<string, string> = {
"1:1": "square_hd",
"16:9": "landscape_16_9",
"9:16": "portrait_16_9",
"4:3": "landscape_4_3",
"3:4": "portrait_4_3",
};
return sizeMap[aspectRatio ?? "1:1"] ?? "square_hd";
}
async generateImage(request: GenerateImageRequest): Promise<GenerateImageResult> {
const model = request.model ?? "fal-ai/flux/schnell";
const body: Record<string, unknown> = {
prompt: request.prompt,
image_size: this.parseImageSize(request.aspectRatio),
num_images: request.n ?? 1,
output_format: "png",
sync_mode: true,
};
if (request.negativePrompt) body.negative_prompt = request.negativePrompt;
if (request.steps) body.num_inference_steps = request.steps;
if (request.seed != null) body.seed = request.seed;
const res = await fetch(`${API_BASE}/${model}`, {
method: "POST",
headers: {
Authorization: `Key ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`fal.ai error: ${res.status} ${await res.text()}`);
}
const data = (await res.json()) as {
images: Array<{
url: string;
width: number;
height: number;
content_type?: string;
}>;
};
// Download images and convert to base64
const images = await Promise.all(
data.images.map(async (img) => {
const imgRes = await fetch(img.url);
const arrayBuf = await imgRes.arrayBuffer();
const base64 = Buffer.from(arrayBuf).toString("base64");
return {
base64,
mimeType: img.content_type ?? "image/png",
url: img.url,
};
})
);
return {
images,
model,
provider: "fal",
};
}
}

43
src/providers/index.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { ImageProvider } from "../types.js";
import { OpenRouterProvider } from "./openrouter.js";
import { TogetherProvider } from "./together.js";
import { ReplicateProvider } from "./replicate.js";
import { FalProvider } from "./fal.js";
export function createProviders(): Map<string, ImageProvider> {
const providers = new Map<string, ImageProvider>();
const openrouterKey = process.env["OPENROUTER_API_KEY"];
if (openrouterKey) {
providers.set("openrouter", new OpenRouterProvider(openrouterKey));
}
const togetherKey = process.env["TOGETHER_API_KEY"];
if (togetherKey) {
providers.set("together", new TogetherProvider(togetherKey));
}
const replicateKey = process.env["REPLICATE_API_TOKEN"];
if (replicateKey) {
providers.set("replicate", new ReplicateProvider(replicateKey));
}
const falKey = process.env["FAL_KEY"];
if (falKey) {
providers.set("fal", new FalProvider(falKey));
}
return providers;
}
export function getDefaultProvider(providers: Map<string, ImageProvider>): ImageProvider | undefined {
// Priority: openrouter > together > fal > replicate
return (
providers.get("openrouter") ??
providers.get("together") ??
providers.get("fal") ??
providers.get("replicate")
);
}
export { OpenRouterProvider, TogetherProvider, ReplicateProvider, FalProvider };

138
src/providers/openrouter.ts Normal file
View File

@@ -0,0 +1,138 @@
import type { ImageProvider, ImageModel, GenerateImageRequest, GenerateImageResult } from "../types.js";
const API_BASE = "https://openrouter.ai/api/v1";
export class OpenRouterProvider implements ImageProvider {
name = "openrouter";
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private headers(): Record<string, string> {
return {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/mcp-image-generator",
"X-Title": "MCP Image Generator",
};
}
async listModels(): Promise<ImageModel[]> {
const res = await fetch(`${API_BASE}/models?output_modalities=image`, {
headers: this.headers(),
});
if (!res.ok) {
throw new Error(`OpenRouter models API error: ${res.status} ${await res.text()}`);
}
const data = (await res.json()) as {
data: Array<{
id: string;
name: string;
description?: string;
pricing?: { prompt?: string; completion?: string; image?: string };
architecture?: {
input_modalities?: string[];
output_modalities?: string[];
};
}>;
};
return data.data.map((m) => ({
id: m.id,
name: m.name,
provider: "openrouter",
description: m.description,
pricing: m.pricing?.image ?? m.pricing?.completion,
inputModalities: m.architecture?.input_modalities ?? ["text"],
outputModalities: m.architecture?.output_modalities ?? ["image"],
modalities: this.getModalities(m.architecture?.output_modalities ?? ["image"]),
supportedAspectRatios: [
"1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9", "9:21",
],
supportedSizes: ["1K", "2K", "4K"],
}));
}
private getModalities(outputModalities: string[]): string[] {
if (outputModalities.includes("text") && outputModalities.includes("image")) {
return ["image", "text"];
}
return ["image"];
}
async generateImage(request: GenerateImageRequest): Promise<GenerateImageResult> {
const model = request.model ?? "google/gemini-2.5-flash-image";
// Determine modalities based on model
const isImageOnly = model.includes("flux") || model.includes("sourceful") || model.includes("riverflow");
const modalities = isImageOnly ? ["image"] : ["image", "text"];
const body: Record<string, unknown> = {
model,
modalities,
messages: [
{
role: "user",
content: request.prompt,
},
],
};
const imageConfig: Record<string, unknown> = {};
if (request.aspectRatio) imageConfig.aspect_ratio = request.aspectRatio;
if (request.imageSize) imageConfig.image_size = request.imageSize;
if (Object.keys(imageConfig).length > 0) body.image_config = imageConfig;
const res = await fetch(`${API_BASE}/chat/completions`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify(body),
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`OpenRouter generation error: ${res.status} ${errText}`);
}
const data = (await res.json()) as {
model: string;
choices: Array<{
message: {
content?: string;
images?: Array<{ image_url: { url: string } }>;
};
}>;
usage?: {
prompt_tokens?: number;
total_tokens?: number;
cost?: number;
};
};
const message = data.choices[0]?.message;
const images =
message?.images?.map((img) => {
const url = img.image_url.url;
// Strip data URL prefix to get raw base64
const base64 = url.replace(/^data:image\/\w+;base64,/, "");
const mimeMatch = url.match(/^data:(image\/\w+);base64,/);
const mimeType = mimeMatch?.[1] ?? "image/png";
return { base64, mimeType, url };
}) ?? [];
return {
images,
model: data.model,
provider: "openrouter",
usage: {
promptTokens: data.usage?.prompt_tokens,
totalTokens: data.usage?.total_tokens,
cost: data.usage?.cost,
},
};
}
}

126
src/providers/replicate.ts Normal file
View File

@@ -0,0 +1,126 @@
import type { ImageProvider, ImageModel, GenerateImageRequest, GenerateImageResult } from "../types.js";
const API_BASE = "https://api.replicate.com/v1";
const REPLICATE_IMAGE_MODELS: ImageModel[] = [
{
id: "black-forest-labs/flux-schnell",
name: "FLUX Schnell",
provider: "replicate",
description: "Fast, high-quality image generation",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "black-forest-labs/flux-1.1-pro",
name: "FLUX 1.1 Pro",
provider: "replicate",
description: "Professional quality FLUX model",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "stability-ai/sdxl",
name: "Stable Diffusion XL",
provider: "replicate",
description: "Stable Diffusion XL on Replicate",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "ideogram-ai/ideogram-v2-turbo",
name: "Ideogram V2 Turbo",
provider: "replicate",
description: "Excellent text rendering in images",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
];
export class ReplicateProvider implements ImageProvider {
name = "replicate";
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async listModels(): Promise<ImageModel[]> {
return REPLICATE_IMAGE_MODELS;
}
async generateImage(request: GenerateImageRequest): Promise<GenerateImageResult> {
const model = request.model ?? "black-forest-labs/flux-schnell";
const input: Record<string, unknown> = {
prompt: request.prompt,
};
if (request.aspectRatio) input.aspect_ratio = request.aspectRatio;
if (request.negativePrompt) input.negative_prompt = request.negativePrompt;
if (request.steps) input.num_inference_steps = request.steps;
if (request.seed != null) input.seed = request.seed;
if (request.n) input.num_outputs = request.n;
// Create prediction
const createRes = await fetch(`${API_BASE}/models/${model}/predictions`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
Prefer: "wait=60",
},
body: JSON.stringify({ input }),
});
if (!createRes.ok) {
throw new Error(`Replicate error: ${createRes.status} ${await createRes.text()}`);
}
let prediction = (await createRes.json()) as {
id: string;
status: string;
output?: string[];
error?: string;
urls?: { get?: string };
};
// Poll if not yet complete
if (prediction.status !== "succeeded" && prediction.status !== "failed") {
const getUrl = prediction.urls?.get ?? `${API_BASE}/predictions/${prediction.id}`;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 2000));
const pollRes = await fetch(getUrl, {
headers: { Authorization: `Bearer ${this.apiKey}` },
});
prediction = (await pollRes.json()) as typeof prediction;
if (prediction.status === "succeeded" || prediction.status === "failed") break;
}
}
if (prediction.status === "failed") {
throw new Error(`Replicate prediction failed: ${prediction.error}`);
}
// Download images and convert to base64
const images = await Promise.all(
(prediction.output ?? []).map(async (url) => {
const imgRes = await fetch(url);
const arrayBuf = await imgRes.arrayBuffer();
const base64 = Buffer.from(arrayBuf).toString("base64");
const contentType = imgRes.headers.get("content-type") ?? "image/webp";
return { base64, mimeType: contentType, url };
})
);
return {
images,
model,
provider: "replicate",
};
}
}

124
src/providers/together.ts Normal file
View File

@@ -0,0 +1,124 @@
import type { ImageProvider, ImageModel, GenerateImageRequest, GenerateImageResult } from "../types.js";
const API_BASE = "https://api.together.xyz/v1";
const TOGETHER_IMAGE_MODELS: ImageModel[] = [
{
id: "black-forest-labs/FLUX.1-schnell",
name: "FLUX.1 Schnell",
provider: "together",
description: "Fast image generation (free tier available)",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "black-forest-labs/FLUX.1-dev",
name: "FLUX.1 Dev",
provider: "together",
description: "Development quality FLUX model",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "black-forest-labs/FLUX.1.1-pro",
name: "FLUX 1.1 Pro",
provider: "together",
description: "Professional quality FLUX model",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
{
id: "stabilityai/stable-diffusion-xl-base-1.0",
name: "Stable Diffusion XL",
provider: "together",
description: "Stable Diffusion XL base model",
inputModalities: ["text"],
outputModalities: ["image"],
modalities: ["image"],
},
];
export class TogetherProvider implements ImageProvider {
name = "together";
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async listModels(): Promise<ImageModel[]> {
return TOGETHER_IMAGE_MODELS;
}
private parseSize(aspectRatio?: string, imageSize?: string): { width: number; height: number } {
const baseSize = imageSize === "2K" ? 2048 : imageSize === "4K" ? 2048 : 1024;
if (!aspectRatio || aspectRatio === "1:1") {
return { width: baseSize, height: baseSize };
}
const ratioMap: Record<string, [number, number]> = {
"16:9": [1344, 768],
"9:16": [768, 1344],
"4:3": [1152, 896],
"3:4": [896, 1152],
"3:2": [1216, 832],
"2:3": [832, 1216],
};
const dims = ratioMap[aspectRatio];
if (dims) return { width: dims[0], height: dims[1] };
return { width: baseSize, height: baseSize };
}
async generateImage(request: GenerateImageRequest): Promise<GenerateImageResult> {
const model = request.model ?? "black-forest-labs/FLUX.1-schnell";
const { width, height } = this.parseSize(request.aspectRatio, request.imageSize);
const body: Record<string, unknown> = {
model,
prompt: request.prompt,
width,
height,
n: request.n ?? 1,
response_format: "b64_json",
};
if (request.negativePrompt) body.negative_prompt = request.negativePrompt;
if (request.steps) body.steps = request.steps;
if (request.seed != null) body.seed = request.seed;
const res = await fetch(`${API_BASE}/images/generations`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`Together AI error: ${res.status} ${await res.text()}`);
}
const data = (await res.json()) as {
model: string;
data: Array<{ b64_json?: string; url?: string }>;
};
const images = data.data.map((d) => ({
base64: d.b64_json ?? "",
mimeType: "image/png" as const,
url: d.url,
}));
return {
images,
model: data.model ?? model,
provider: "together",
};
}
}

47
src/types.ts Normal file
View File

@@ -0,0 +1,47 @@
export interface ImageModel {
id: string;
name: string;
provider: string;
description?: string;
pricing?: string;
inputModalities: string[];
outputModalities: string[];
modalities: string[];
supportedAspectRatios?: string[];
supportedSizes?: string[];
}
export interface GenerateImageRequest {
prompt: string;
model?: string;
provider?: string;
aspectRatio?: string;
imageSize?: string;
negativePrompt?: string;
steps?: number;
seed?: number;
n?: number;
}
export interface GenerateImageResult {
images: GeneratedImage[];
model: string;
provider: string;
usage?: {
promptTokens?: number;
totalTokens?: number;
cost?: number;
};
}
export interface GeneratedImage {
base64: string;
mimeType: string;
url?: string;
}
export interface ImageProvider {
name: string;
listModels(): Promise<ImageModel[]>;
generateImage(request: GenerateImageRequest): Promise<GenerateImageResult>;
}

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"module": "nodenext",
"target": "es2022",
"types": ["node"],
"lib": ["es2022"],
"sourceMap": true,
"declaration": true,
"strict": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"moduleDetection": "force",
"skipLibCheck": true,
"moduleResolution": "nodenext",
"esModuleInterop": true
},
"include": ["src/**/*"]
}