# App API - Image Generation Service

Image generation is available for every project with no setup needed (user_pays billing by default). **Building a no-login app** (like a public photo editor)? `user_pays` bills the signed-in visitor, so anonymous callers get `401 LOGIN_REQUIRED` — set `image` to `owner_pays` so it works for everyone (see billing below).

> **Plan availability:** Image generation is limited to **3 uses per month** on the Free plan, then unlimited on Pro. When a Free-plan owner exceeds the cap, this endpoint returns **403 FORBIDDEN** with an upgrade message - handle that response in the app's UI.

## Two modes: generate OR edit

- **Generate** (text → image): send just a `prompt`, get a brand-new image.
- **Edit** (image + instruction → image): send `prompt` **and** an `images` array of source image(s). The prompt is an edit instruction applied to them — "make it night", "add a hat", "remove the car in the background", "turn this into a watercolor". This is how you build a photo editor: upload → edit → show before/after.

Editing runs on Gemini automatically (its image model does instruction editing natively) — you don't pick a provider or model for it, just pass `images`. No GPU job, no custom model, no img2img endpoint to stand up. Same `{ url, credits_used, … }` response as generation.

For purely mechanical changes that don't need a model — crops, rotations, simple filters, overlays/compositing — do it **client-side** with canvas/WebGL (free, instant, no round trip).

Use `project_settings` to customize (optional):
- Switch billing mode (owner_pays ↔ user_pays)
- Restrict allowed providers
- Set default provider/model

> To make the billing choice reproducible (it ships with the app instead of living as out-of-band server state), declare it in a `type: services` deploy phase in `gipity.yaml` instead of only via `project_settings`. The definitions go under `service_definitions:` (not `services:`):
> ```yaml
> - name: services
>   type: services
>   service_definitions:
>     - service: image
>       billing_mode: owner_pays   # you pay; no visitor login needed
> ```
> See [deploy](deploy.md) for the full services-phase reference and [app-llm](app-llm.md) for billing modes.

## Providers & Models
- **OpenAI**: `gpt-image-2`
- **BFL/Flux**: `flux-2-pro, flux-2-flex, flux-2-max, flux-2-klein-9b, flux-2-klein-4b`
- **Gemini/Nano Banana**: `gemini-3.1-flash-lite-image, gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image`

## Quality tiers (recommended over pinning a model)

Instead of a concrete model id, pass a **quality tier** as `model` — it resolves to a current provider+model that we keep pointed at the best option, so your app keeps working when the model lineup refreshes (a pinned concrete id 400s the day it's retired). A tier sets the provider for you (overriding `provider`):

- `fast` — cheapest, fastest; for thumbnails, drafts, high-volume generation
- `standard` — balanced general-purpose quality (the sensible default)
- `high` — high fidelity; for hero images and detailed scenes
- `ultra` — maximum fidelity; slowest and most expensive

```json
{ "prompt": "A sunset over mountains", "model": "high" }
```

`GET /image/models` returns the live tier→provider/model map (`tiers`) alongside the concrete model list. Pin a concrete id only when you specifically need that exact model.

## Endpoints
- `GET /api/<PROJECT_GUID>/services/image/models` - list available providers and models
- `POST /api/<PROJECT_GUID>/services/image` - generate an image

## Request Format (POST /image)
```json
{
  "prompt": "A sunset over mountains",
  "provider": "openai",
  "model": "gpt-image-2",
  "size": "1024x1024",
  "quality": "auto"
}
```

Fields:
- `prompt` (required): Image description (generate) **or** edit instruction (edit), max 4,000 chars
- `images`: **Edit mode.** Array (1-4) of source images to edit — each `{ "data": "<base64>", "media_type": "image/png" }` (jpeg/png/gif/webp). When present, `prompt` is an edit instruction applied to these images and the request routes to Gemini (any `provider`/`model` you pass is ignored for edits). Omit for plain generation.
- `provider`: "openai", "bfl", or "gemini" (default: bfl)
- `model`: A quality **tier** — `fast`, `standard`, `high`, or `ultra` (recommended; survives model refreshes and sets the provider) — OR a concrete model id (default: provider's default). See Quality tiers above.
- `size`: "WxH" format (default: 1024x1024). OpenAI sizes: 1024x1024, 1024x1536, 1536x1024. BFL: any size (rounded to 32px). Gemini: mapped to nearest resolution tier
- `quality`: OpenAI gpt-image-2: low/medium/high/auto
- `aspect_ratio`: Gemini only. Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9
- `image_size`: Gemini only. Resolution tier: 512, 1K, 2K, 4K (default: 1K)
- `seed`: BFL only (ignored by openai/gemini). A non-negative integer that makes generation deterministic — reuse one seed across a series of prompts to keep the images visually coherent (e.g. every illustration in one clip). Omit it for fresh randomness each call.

## Response Format
```json
{
  "url": "https://media.gipity.ai/med_abc12345.png",
  "content_type": "image/png",
  "revised_prompt": "...",
  "model": "gpt-image-2",
  "provider": "openai",
  "credits_used": 50
}
```

The `url` is a permanent public CDN URL. No auth needed to fetch it.

BFL responses also include a `seed` field — the seed that produced this image (the one you passed, or the random one BFL picked). Capture it and pass it back as `seed` on later calls to keep a series of images visually coherent.

### CLI
For one-off image generation during development (downloads the result to a local file), skip the HTTP call and use `gipity generate image`. **It writes to `./generated.png` in the current directory by default** - pass `-o <path>` to land the file directly in your source tree so it deploys:
```bash
gipity generate image "a cat wearing a top hat" -o src/assets/images/hat-cat.png
gipity generate image "landscape sunset" --provider gemini --aspect-ratio 16:9 -o src/assets/images/hero.png
gipity generate image "product photo" --provider openai --model gpt-image-2 --size 1536x1024 --quality high -o src/assets/images/product.png
```
To **edit** an existing image (the photo-editor flow) from the CLI, pass `--input <file>` — the prompt becomes an edit instruction and it routes to Gemini automatically (same as the `images` app-service param). Use this to verify the edit end-to-end without hand-rolling a token + base64 POST:
```bash
gipity generate image "make it night time" --input photo.jpg -o after.png
```

## Client Code Example

**Never hardcode `a.gipity.ai` as the base.** That host is only correct on prod; an app deployed by any other instance (dev, local, self-hosted) must call the instance it was deployed to. The deploy injects `<script src=".../client/v1/gipity.js" data-app data-api-base>` into your `<head>`, so read the app guid and API base straight off that tag — it's always the right instance.

The image service replies with a **flat body** — `url`, `content_type`, `model`, `credits_used` at the TOP level (no `{ data }` wrapper). Read `res.json().url` directly.

```js
// Read the app guid + API base the deploy stamped onto the SDK script tag.
const sdk = document.querySelector('script[data-api-base]');
const apiBase = sdk.dataset.apiBase;      // this instance's API — NOT a hardcoded a.gipity.ai
const app = sdk.dataset.app;              // your PROJECT_GUID

// 1. Mint an app token (POST, the token is nested under data)
const { data: { token } } = await (await fetch(`${apiBase}/api/token`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ app }),
})).json();

// 2. Generate an image — fields are at the top level of the response
const res = await fetch(`${apiBase}/api/${app}/services/image`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-App-Token': token },
  body: JSON.stringify({ prompt: 'A cat wearing a top hat' }),
});
const { url } = await res.json();          // { url, content_type, model, credits_used }

// 3. Display
const img = document.createElement('img');
img.src = url;                             // permanent public CDN URL
document.body.appendChild(img);
```

### Editing an uploaded photo (before/after)

A photo editor uploads the user's picture, sends it with an edit instruction, and shows the result next to the original. Read the file the user picked as base64 and pass it as `images` (reuse `apiBase`, `app`, and `token` from above):

```js
// file is a File from <input type="file">
const dataUrl = await new Promise((r) => {
  const fr = new FileReader();
  fr.onload = () => r(fr.result);
  fr.readAsDataURL(file);
});
const base64 = dataUrl.split(',')[1];              // strip the "data:...;base64," prefix
const media_type = file.type;                       // e.g. "image/png"

const res = await fetch(`${apiBase}/api/${app}/services/image`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-App-Token': token },
  body: JSON.stringify({
    prompt: 'make it night time',                   // the plain-English edit
    images: [{ data: base64, media_type }],
  }),
});
const { url } = await res.json();                   // top-level url of the edited image
// Show it beside the original for a before/after.
```

Keep inline images reasonable (the request body caps at 10 MB — a base64 photo is ~1.33× its byte size). Downscale a huge camera photo on a canvas before sending, or for very large originals upload via app-files first. From a **serverless function** it's the same, via the injected `image()` service:

```js
const edited = await services.image({ prompt: 'add a party hat', images: [{ data, media_type: 'image/jpeg' }] });
```

## Limits
- **Rate limit**: 600 requests per 5-minute window (per IP)
- **Max prompt length**: 4,000 chars
- **Timeout**: 120s
- Standard `RateLimit-*` headers included in responses
