4.0 KiB
Image Generation
Use OpenRouter image-capable chat models for image generation or image editing, then store durable outputs in InsForge Storage.
Official OpenRouter references:
Setup
Start with overview.md: run npx -y @insforge/cli ai setup and keep
OPENROUTER_API_KEY server-side. Check image-capable models before use:
curl "https://openrouter.ai/api/v1/models?output_modalities=image" \
-H "Authorization: Bearer $OPENROUTER_API_KEY"
Generate an Image
OpenRouter image generation uses chat completions with image output
modalities. Direct fetch avoids TypeScript friction around OpenRouter-specific
fields such as modalities, image_config, and message.images.
const prompt = 'Create a clean product mockup on a white desk.'
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: process.env.OPENROUTER_IMAGE_MODEL ?? 'google/gemini-2.5-flash-image',
modalities: ['image', 'text'],
messages: [
{ role: 'user', content: prompt },
],
image_config: {
aspect_ratio: '16:9',
},
}),
})
if (!response.ok) {
throw new Error(`OpenRouter image request failed: ${response.status}`)
}
const result = await response.json()
const imageUrl = result.choices?.[0]?.message?.images?.[0]?.image_url?.url
if (!imageUrl) {
throw new Error('OpenRouter response did not include an image URL')
}
Store the Output
OpenRouter output URLs should be treated as transfer URLs, not your app's
permanent asset store. Upload the generated file to InsForge Storage and save
both url and key in the database.
Do not store raw base64 image data or large binary blobs in Postgres. Store metadata and Storage references in the database; keep image bytes in Storage. Derive owner, tenant, or session fields from the authenticated server-side context; never accept ownership IDs from the browser.
const imageResponse = await fetch(imageUrl)
const imageBlob = await imageResponse.blob()
const storageKey = `generated/${crypto.randomUUID()}.png`
const { data, error } = await insforge.storage
.from('images')
.upload(storageKey, imageBlob)
if (error) {
throw error
}
const { data: userData, error: userError } = await insforge.auth.getCurrentUser()
if (userError || !userData?.user?.id) {
throw userError ?? new Error('Authentication required')
}
await insforge.database.from('generated_images').insert([{
user_id: userData.user.id,
prompt,
image_url: data.url,
image_key: data.key,
}])
Best Practices
- Use models-list.md to verify
output_modalitiesincludesimage. - Keep image prompts and model calls on the server.
- Persist generated images in InsForge Storage before storing database rows.
- Save the original prompt, model ID, storage
url, and storagekeywhen the product needs reproducibility or deletion. - Use authenticated owner, session, or tenant fields on generated-image rows so RLS can restrict access.
- Use direct HTTP when OpenAI SDK types reject OpenRouter-specific image fields; do not remove those fields to satisfy TypeScript.
Common Mistakes
| Mistake | Fix |
|---|---|
| Using a text-only model for image output | Filter models with output_modalities=image |
| Storing raw base64 image data or temporary image URLs in Postgres | Upload to Storage, save url and key |
Removing modalities because TypeScript complains |
Use direct fetch or a narrow local type cast |
| Assuming every image model supports the same options | Check the model and image generation docs first |