Files
insforge/ai/image-generation.md

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

  1. Use models-list.md to verify output_modalities includes image.
  2. Keep image prompts and model calls on the server.
  3. Persist generated images in InsForge Storage before storing database rows.
  4. Save the original prompt, model ID, storage url, and storage key when the product needs reproducibility or deletion.
  5. Use authenticated owner, session, or tenant fields on generated-image rows so RLS can restrict access.
  6. 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