# POST /v1/images/generations

Generate images from a text prompt. The route follows the OpenAI Images API, so
`client.images.generate` works unchanged with the base URL `https://demo-api.routerplus.com/v1`.
`https://demo-api.routerplus.com/images/generations` is an alias for SDKs that omit the `/v1` prefix.

To try it without code, pick an image model in the [playground](/docs/playground).

- The route returns one JSON response by default. Existing clients need no change.
- On the development API, `stream: true` is optional. It returns keep-alives and then one
  final image event. See [Final-image streaming](#final-image-streaming).
- Images always come back as base64, with no partial previews.
- OpenAI serves the GPT Image models directly. Replicate, Krea and Recraft serve the other
  models. See [Krea image models](#krea-image-models),
  [Models served by Replicate](#models-served-by-replicate) and [Recraft models](#recraft-models).

## Examples

Each example generates one image and decodes it to `out.png`.

curl:

```bash
curl -s https://demo-api.routerplus.com/v1/images/generations \
  -H "Authorization: Bearer $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"gpt-image-1","prompt":"a red bicycle on a white background","size":"1024x1024","quality":"medium"}' \
  | jq -r '.data[0].b64_json' | base64 -d > out.png
```

Python:

```python
import base64
import os
from openai import OpenAI

client = OpenAI(base_url="https://demo-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"])

r = client.images.generate(
    model="gpt-image-1",
    prompt="a red bicycle on a white background",
    size="1024x1024",
    quality="medium",
)
with open("out.png", "wb") as f:
    f.write(base64.b64decode(r.data[0].b64_json))
print(r.usage.model_dump()["cost"])  # the exact ledger debit, in USD
```

TypeScript:

```typescript
import { writeFileSync } from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://demo-api.routerplus.com/v1", apiKey: process.env.TM_API_KEY });

const r = await client.images.generate({
  model: "gpt-image-1",
  prompt: "a red bicycle on a white background",
  size: "1024x1024",
  quality: "medium",
});

const b64 = r.data?.[0]?.b64_json ?? "";
writeFileSync("out.png", Buffer.from(b64, "base64"));
console.log(r.usage); // input/output tokens and cost (USD)
```

Send your key as `Authorization: Bearer tm_vk_...` or as `x-api-key: tm_vk_...`. A missing or
invalid key returns 401 with `error_type: "auth"`. You can send `HTTP-Referer` and `X-Title` to
identify your app. They carry attribution only, no content.

## Request

The body is JSON, at most 10 MB. A larger body is a 413 `request_too_large`. RouterPlus checks
every rule below before it reserves money or dispatches. A refused request has no ledger row,
no `x-tm-attempts` header and no charge.

| Parameter | Type | Behavior |
|---|---|---|
| `model` | string, required | An image model id. See [Model discovery](#model-discovery). A dated pin (`-20260421`, `-2026-04-21`, `-latest`) resolves to its catalog family for routing and pricing, and your exact string goes upstream. A chat model is a 400 that names the chat routes. A decision model is a 400 that names `/v1/decisions`. An id that is not an image model anywhere is a 404. Omitted or empty is a 400 that names `model`. |
| `prompt` | string, required | Non-empty, at most 32 000 characters. |
| `n` | integer | 1 to 4 on the GPT Image models. Defaults to 1. The SDK allows 10, but four buffered images already approach the response cap. |
| `size` | string | `auto`, `1024x1024`, `1536x1024` or `1024x1536` on the GPT Image models. |
| `quality` | string | `auto`, `low`, `medium`, `high`. Also `xhigh` and `max` on `gpt-image-2.5-flare` and `gpt-image-2.5-sunburst`. |
| `background` | string | `auto`, `transparent`, `opaque` on the GPT Image models. |
| `moderation` | string | `auto` or `low` on the GPT Image models. |
| `output_format` | string | `png`, `jpeg` or `webp` on the GPT Image models. |
| `output_compression` | integer | 0 to 100 on the GPT Image models. |
| `user` | string | Forwarded as sent. Any type other than a string is a 400. |
| `response_format` | string | `b64_json` only, and it has no effect. `url` is a 400. See [Always base64](#always-base64). |
| `stream` | boolean | `true` turns on final-image SSE and requires `n: 1`. Absent, `null` or `false` returns the ordinary JSON response. |
| `partial_images` | integer | Absent, `null` or `0` only. RouterPlus does not support partial previews. Other values are a 400 before dispatch. |
| `provider` | object | Routing controls, including `require_parameters`. RouterPlus reads them and never forwards them. See [Routing policies](/docs/routing-policies). |

Each model publishes its accepted values for `size`, `quality`, `background`, `moderation`,
`output_format` and `output_compression` in `/api/models.json` under `supported_parameters`.

- A value outside the declared set of a model is a 400. The message starts with the field
  name and lists what that model accepts.
- A parameter from this list that a model does not declare is dropped and recorded, like any
  unknown key.
- An explicit `null` means "not provided" for every optional parameter above. The OpenAI SDKs
  send an unset optional as `null`, so
  `client.images.generate(model=…, prompt=…, n=None, size=None)` works. RouterPlus never
  forwards a `null` and never records it as a drop.
- `model` and `prompt` are required, so `null` there is a 400.

### Everything else

RouterPlus validates and forwards the fields that the selected model declares in
`supported_parameters`. This includes the native Krea reference and editing parameters.

RouterPlus drops and records undeclared keys, and never forwards them. The dropped names are:

- comma-joined in the `x-tm-dropped-params` response header,
- stored on the ledger row of the attempt,
- visible at `GET /v1/generation?id=`.

To get a 400 instead of a drop, send `"provider": {"require_parameters": true}`. The 400 comes
before RouterPlus reserves money.

### Always base64

RouterPlus always returns the image bytes inline as `b64_json`. There is no hosted URL: storing
images would put your content on RouterPlus disks, which the
[data policy](/docs/data-policy) forbids. `response_format: "url"` is a typed 400. Decode the
base64 yourself, as the [examples](#examples) do.

## Response

Without `stream: true`, the answer is HTTP 200, `application/json`. The body is OpenAI's
`ImagesResponse`, relayed unchanged except for one added field, `usage.cost`:

```json
{
  "created": 1790000000,
  "data": [{ "b64_json": "iVBORw0KGgoAAAANSUhEUg..." }],
  "background": "opaque",
  "output_format": "png",
  "quality": "medium",
  "size": "1024x1024",
  "usage": {
    "input_tokens": 12,
    "input_tokens_details": { "text_tokens": 12, "image_tokens": 0 },
    "output_tokens": 1056,
    "total_tokens": 1068,
    "cost": 0.0423
  }
}
```

| Field | Meaning |
|---|---|
| `data[].b64_json` | The image, base64-encoded. One entry per image the provider rendered, normally `n`. RouterPlus relays what it received and bills what the provider reported. |
| `data[].revised_prompt` | The provider's rewritten prompt, when it sends one. See [Data policy](/docs/data-policy). |
| `usage.input_tokens` | Prompt tokens, billed at the `prompt` rate of the model. |
| `usage.output_tokens` | Image output tokens, billed at the `completion` rate of the model. |
| `usage.input_tokens_details` | For information only. The bill uses the two counts above. |
| `usage.total_tokens` | `input_tokens + output_tokens`. |
| `usage.cost` | USD, the full request debit, including any attempt that failed over before this one. `0` on BYOK. Absent on static dev keys. |

RouterPlus never adds the chat field `prompt_tokens_details` to an images response.

> [!NOTE]
> The provider can return a 200 without a usage object, or with `input_tokens` and
> `output_tokens` both 0. Then RouterPlus bills `n` × the per-image ceiling of the model, with
> provenance `estimated`. That is the amount the reservation held. RouterPlus writes those
> numbers into `usage`, so your copy of the bill still adds up. See the provenance at
> [GET /v1/generation](/docs/api-usage).

### Long-running JSON requests

Image generation can take several minutes. You do not need to change the request or the SDK.
An ordinary request keeps its final HTTP status, complete JSON body, headers and billing. Every
image count the model supports still works.

While the image is pending, the edge relay keeps its origin connection active, then returns the
original result. This avoids
[Cloudflare's default 125-second proxy read timeout](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/).
The relay stores no image content and starts no second generation.

The generation budget is 450 seconds. Set the overall timeout of your client to at least that.
Do not retry a lost response automatically: the provider may already have accepted the render.

### Final-image streaming

Send `"stream": true` with `"n": 1` for a render that may take several minutes.

1. After validation, the response starts as HTTP 200 with `Content-Type: text/event-stream`.
2. RouterPlus sends a comment at once, then a keep-alive comment every 15 seconds while the
   image is pending. Comments keep the connection active. They are not partial images or
   progress estimates.
3. The final event follows the
   [OpenAI image completion event](https://developers.openai.com/api/reference/resources/images/generation-streaming-events).
4. The connection closes.

```text
: rendering

event: image_generation.completed
data: {"type":"image_generation.completed","created_at":1790000000,"b64_json":"iVBORw0KGgoAAAANSUhEUg...","usage":{"cost":0.04},"request_id":"gen_example","response_headers":{"x-tm-provider":"krea","x-tm-attempts":"1"}}

```

| Field | Meaning |
|---|---|
| `b64_json` | The final image. |
| `created_at` | The response timestamp. |
| `size`, `quality`, `background`, `output_format` | Present only when known from the result. RouterPlus does not report requested values as measured output. |
| `usage` | The same accounting as the JSON response, including `usage.cost` where available. |
| `request_id` | RouterPlus extension. |
| `response_headers` | RouterPlus extension. The HTTP headers are already sent, so this carries the final routing headers, such as `x-tm-provider`, `x-tm-attempts` and `x-tm-upstream-status`. |

There is one completed event, with no partial-image events and no `[DONE]` event. The
`x-request-id` HTTP header identifies the request from the start.

An error found before the stream starts keeps its normal HTTP status and JSON body. After the
stream starts, a failed request ends with an `error` event in place of the completion:

```text
event: error
data: {"type":"error","error":{"code":"upstream_error","message":"Image generation failed.","type":"api_error","metadata":{"error_type":"upstream_error"}},"request_id":"gen_example","status":502,"response_headers":{"x-tm-error-code":"upstream_error"}}

```

- `status` is the HTTP-equivalent status of the failure. The open SSE response stays HTTP 200.
- `error` and `response_headers` carry the error details and the final routing headers.
- The OpenAI SDK raises streaming errors.
- Do not resubmit a disconnected stream or a timed-out request automatically. The provider may
  already have accepted the render. Use the request ID to read
  [generation usage](/docs/api-usage).

SSE is optional on the development API. Plain JSON uses the edge relay in
[Long-running JSON requests](#long-running-json-requests). Neither SSE nor plain JSON extends the generation budget or guarantees that a provider finishes
in time.

These examples keep a slow Qwen render connected and print its final event. The Python example
turns automatic retries off, so a lost response does not submit another render.

curl:

```bash
curl -N --max-time 480 https://demo-api.routerplus.com/v1/images/generations \
  -H "Authorization: Bearer $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"krea/alibaba/qwen-image-3","prompt":"a red bicycle on a white background","width":1024,"height":1024,"n":1,"stream":true,"partial_images":0}'
```

Python:

```python
import base64
import os
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    base_url="https://demo-api.routerplus.com/v1",
    api_key=os.environ["TM_API_KEY"],
    timeout=480.0,
    max_retries=0,
)

stream = client.images.generate(
    model="krea/alibaba/qwen-image-3",
    prompt="a red bicycle on a white background",
    n=1,
    stream=True,
    partial_images=0,
    extra_body={"width": 1024, "height": 1024},
)
for event in stream:
    if event.type == "image_generation.completed":
        Path("out.image").write_bytes(base64.b64decode(event.b64_json))
        usage = event.usage.model_dump() if event.usage is not None else {}
        print(usage.get("cost"))
```

The file holds the provider's image bytes. Use `event.output_format` for the file extension
when it is present. Otherwise inspect the file type.

## Billing

Images use the same two SKUs and the same integer micro-USD math as chat
([Pricing & billing](/docs/pricing)). Text input bills at `prompt`, and image output bills at
`completion`.

- **Reserve**, before RouterPlus contacts a provider: prompt bytes ÷ 4 at the `prompt` rate,
  plus `n` × the per-image ceiling of the model at the `completion` rate, rounded up. The chat
  `max_tokens` default plays no part.
- **Settle** on the counts the provider reported, rounded down. RouterPlus releases the hold in
  full.

Example on `gpt-image-1` (ceiling 6240 tokens, $5 / $40 per million):

| Case | Amount |
|---|---|
| Reserve, `n: 1` | 6240 × $40/M ≈ $0.2496, plus the prompt estimate |
| Reserve, `n: 4` | 4 × 6240 × $40/M ≈ $0.9984, plus the prompt estimate |
| Settle, one medium 1024×1024 image | 1056 × $40/M = $0.04224, plus the prompt |

**A failed generation costs nothing.** A failed attempt, a response over the 32 MiB cap and a
200 with no decodable image all settle at zero. After RouterPlus receives a 200, it never
dispatches again, so one request never pays for two renders.

**A cancelled render is billed at what the provider reported.** If you close the connection
mid-render, RouterPlus does not abort the upstream call. The render finishes, RouterPlus reads
the usage, and the attempt settles at the provider's numbers. It is never the reservation and
never $0. RouterPlus writes nothing to your closed socket.

> [!WARNING]
> Reservations are large next to a trial balance. `n: 4` at a high quality holds about a
> dollar, and your balance must cover every reservation your organization has in flight. A
> `429 insufficient_quota` on `n: 4` is expected. Lower `n`, or add credits.

### Krea image models

Krea models have provider-qualified IDs such as `krea/krea-3` and
`krea/google/nano-banana-2.1`. `/api/models.json` lists the current models with the
`supported_parameters`, `required_parameters` and `request_defaults` of each. Structured
parameters carry their JSON schema under `schema`.

Each request renders one image (`n: 1`). RouterPlus runs Krea's asynchronous submit, poll and
download flow. Your request returns the completed OpenAI-style base64 image.

```json
{
  "model": "krea/krea-3",
  "prompt": "Change the mug to blue, keeping everything else the same.",
  "image_urls": ["data:image/png;base64,..."],
  "resolution": "2k",
  "thinking_mode": "fast",
  "regional_edit": true
}
```

Krea 3 parameters:

- `image_urls`: up to eight.
- `negative_prompt`: optional, up to 1,024 characters.
- `seed`.
- `style_urls` (style references): not in Krea's API yet. The field is a 400 before dispatch.
- Regional edits are experimental.
- `thinking_mode`: `fast` or `high`. `ultra` requires `resolution: "2k"` and cannot be
  combined with regional editing.
- Custom canvas: send both `width` and `height` (positive integers up to 4096), and omit
  `resolution` and `aspect_ratio`. Krea rounds the request to a supported canvas, so exact
  pixel dimensions are not guaranteed.
- Otherwise the default is 2K with automatic framing. The model lists its optional
  `aspect_ratio` values.

A Krea 3 reference is a Krea asset URL or an inline PNG, JPEG or WebP data URI. RouterPlus
uploads inline images to Krea's assets endpoint before it submits the job. The playground also
accepts local files. References count toward the 10 MB request limit. Other Krea models accept
the external reference URLs their schemas declare.

Pricing:

- Krea 3 is free during early access.
- Other enabled Krea models charge the fixed USD price for the selected settings and
  references, shown on their model pages.
- RouterPlus holds the published maximum of the model, then settles the selected price only
  after a successful render. It does not convert Krea's in-app compute units into API dollars.
- A model without verified price rules is unavailable until those rules exist.

Qwen Image 3 (`krea/alibaba/qwen-image-3`) and Seedream 5 Pro
(`krea/bytedance/seedream-5-pro`) price the requested total pixels, `width × height`:

| Model | Lower tier, inclusive | Above that area | References |
|---|---|---|---|
| Qwen Image 3 | ≤1,048,576 pixels: $0.04 | $0.075 | Up to 3; included in the price |
| Seedream 5 Pro | ≤2,359,296 pixels: $0.045 | $0.09 | Up to 10; first included, then $0.003 each |

- A Qwen 2048×512 image costs $0.04, the same as 1024×1024.
- A Seedream 3072×768 image costs $0.045 before additional references.
- Crossing the area threshold raises the base price, even when only one dimension changes.
- Qwen defaults to 1024×1024. Seedream defaults to 1536×1536. For another size, send positive
  whole-pixel `width` and `height`. The parameter schema of each model has its dimension bounds.
- The reservation ceilings are $0.075 (Qwen) and $0.117 (Seedream).

### Models served by Replicate

Replicate serves Nano Banana Pro, FLUX.2 [max] and [pro], Seedream 5.0 Lite, Grok Imagine Image
and Wan 2.7 Image Pro. RouterPlus bills them at Replicate's published price for the render,
with no margin ([Pricing & billing](/docs/pricing#models-billed-at-the-providers-reported-cost)).
The reservation is `n` × the per-image ceiling of the model (**Per image ≤**). The charge is
the listed price. These models take `aspect_ratio` (or `size`) and `resolution` in place of
`quality`.

| Model id | Made by | Shape | Listed price | Per image ≤ |
|---|---|---|---|---|
| `nano-banana-pro` | Google | `aspect_ratio`; `resolution` 1K, 2K, 4K | $0.15 at 1K or 2K, $0.30 at 4K | $0.30 |
| `flux-2-max` | Black Forest Labs | `aspect_ratio`; `resolution` 0.5MP, 1MP, 2MP, 4MP; `seed` | $0.04 a render + $0.03 per output megapixel | $0.19 |
| `flux-2-pro` | Black Forest Labs | as `flux-2-max` | $0.015 a render + $0.015 per output megapixel | $0.09 |
| `seedream-5-lite` | ByteDance | `aspect_ratio`; `resolution` 2K, 3K | $0.035 | $0.035 |
| `grok-imagine-image` | xAI | `aspect_ratio` | $0.02 | $0.02 |
| `wan-2.7-image-pro` | Alibaba | `size` (10 exact sizes up to 2048x2048, or `auto`); `n` up to 4, at most two 2048x2048 images' pixels per answer; `seed` | $0.03 | $0.03 |

- Replicate counts FLUX.2 output megapixels in whole megapixels, rounded up. A 0.5MP render
  bills as 1.
- Prices were read from Replicate on 2026-10-12.
- Wan 2.7 Image Pro answers in PNG only, so one answer holds at most 8,388,608 pixels. `n` can
  be up to 4 at the 1K sizes, 3 at 2048x1152 and 1152x2048, and 2 at 2048x1536, 1536x2048,
  2048x2048 and `auto`. A larger `n` is a 400 before anything renders, so it costs nothing.

### Recraft models

Recraft serves its models directly, with IDs of the form `recraft/<model>`, such as
`recraft/recraftv4_1` or `recraft/recraftv2_vector`. RouterPlus bills them at Recraft's
published price, passed through. The charge is the cost Recraft reports for the request.

| Family | Model ids | Raster | Vector | Pro raster (2K) | Pro vector |
|---|---|---|---|---|---|
| V4.1 Flash | `recraft/recraftv4_1_flash` | $0.007 | n/a | n/a | n/a |
| V4.1 | `recraft/recraftv4_1`, `…_vector`, `…_pro`, `…_pro_vector` | $0.035 | $0.08 | $0.21 | $0.30 |
| V4.1 Utility | `recraft/recraftv4_1_utility`, `…_vector`, `…_pro`, `…_pro_vector` | $0.035 | $0.08 | $0.21 | $0.30 |
| V4 Styles | `recraft/recraftv4_styles`, `…_vector`, `…_pro`, `…_pro_vector` | $0.035 | $0.05 | $0.10 | $0.12 |
| V4 | `recraft/recraftv4`, `…_vector`, `…_pro`, `…_pro_vector` | $0.04 | $0.08 | $0.25 | $0.30 |
| V3 | `recraft/recraftv3`, `recraft/recraftv3_vector` | $0.04 | $0.08 | n/a | n/a |
| V2 | `recraft/recraftv2`, `recraft/recraftv2_vector` | $0.022 | $0.044 | n/a | n/a |

Prices are per image. A request with `style_reference_urls` adds $0.005 once, because Recraft
creates a style from the references. The per-image ceiling includes that fee. Prices were read
from Recraft on 2026-10-16.

- **Output:**
  - `_vector` models return SVG. `data[].b64_json` holds the SVG text, and `output_format` is `svg`.
  - Raster models return WebP by default. Send `image_format: "png"` for PNG.
- **Images per request:** `n` takes 1 to 6.
- **`size`:** every model accepts the 14 aspect ratios (`1:1`, `16:9`, `6:10`, …). Raster models also accept their exact `WxH` sizes. Pro models use the 2K ones. Omit `size` to let Recraft choose from the prompt.
- **Styles:**
  - Every model except V4.1 Flash takes `style_id` (a Recraft style) or `style_reference_urls`: one to ten PNG, JPEG or WebP URLs or data URIs. Never both.
  - You can reuse the `style_id` from the response.
  - V4 Styles models require one of the two.
  - `style_match` is `flexible` or `precise` on V4 and newer, and `regular` on V2 and V3.
- **V2 and V3 only:** `style` (a curated style name such as `Photorealism`) and `negative_prompt`. Prompts on V2 and V3 are limited to 1,000 characters. On newer models the limit is 10,000.
- **V3 only:** `text_layout`. `controls` takes `colors` and `background_color` on every model, plus `artistic_level` and `no_text` on V3.
- **Seed:** RouterPlus passes `seed` to Recraft as its `random_seed`.

```json
{
  "model": "recraft/recraftv4_styles_vector",
  "prompt": "a lighthouse on a cliff",
  "size": "16:9",
  "style_reference_urls": ["https://example.com/brand-reference.png"]
}
```

## Errors

| Case | Status | `error_type` | Billed |
|---|---|---|---|
| A field outside the model's accepted values, `n` outside the model's limits, `stream: true` with `n` other than 1, nonzero `partial_images`, `response_format: "url"`, a chat or decision model on this route | 400 | `invalid_request` | no row |
| The model is listed but has no price row | 400 | `model_not_priced` | no row |
| Not an image model in the catalog | 404 | `model_unavailable` | no row |
| Request body over 10 MB | 413 | `request_too_large` | no row |
| Key RPM or a shared limit, balance too low, or a monthly spend cap | 429 | `rate_limit` / `insufficient_quota` | no row |
| OpenAI moderation refusal (`moderation_blocked`) | the provider's, usually 400 | `content_policy` | $0; never rerouted, never a health strike |
| Any other provider 4xx | the provider's | `upstream_error` | $0; no failover |
| Provider 5xx, a failed connection, or the 450 s budget | 5xx relayed / 502 / 504 | `upstream_error` / `model_unavailable` | $0 per failed attempt; fails over like chat |
| Response over the 32 MiB cap | 502 | `gateway_error` | $0; no failover |
| A 200 that is unparseable or has no `b64_json` | 502 | `upstream_error` | $0; no failover |
| The budget ran out while reading the image body | 504 | `upstream_error` | $0; no failover |
| Every deployment serving the model is cooling down | 503 | `gateway_error` | nothing dispatched; `retry-after: 5` |
| The ledger journal, the shared limit store, or the admission lease is unavailable | 503 | `gateway_error` | nothing charged |

- Error bodies use the OpenAI error envelope, with the canonical class in
  `error.metadata.error_type`. Every failure also has the class in `x-tm-error-code`.
- RouterPlus never relays a provider's own error body. The provider's status is in
  `x-tm-upstream-status`.
- After SSE begins, the terminal `error` event carries these details, and the HTTP status can
  no longer change. See [Final-image streaming](#final-image-streaming).
- An image model sent to `POST /v1/chat/completions`, `POST /v1/messages` or
  `POST /v1/decisions` is a 400 `invalid_request` that names `/v1/images/generations`.

[Errors](/docs/errors) lists every error class and its fix.

## Limits

| Limit | Value |
|---|---|
| Images per request | JSON: model limit up to `n` = 4; SSE: `n` = 1 |
| Prompt length | 32 000 characters |
| Request body | 10 MB → 413 `request_too_large` |
| Upstream response | 32 MiB → 502 `gateway_error`, nothing billed. |
| Generation budget | 450 s, covering the body read as well as the render |
| Streaming | Opt-in final-image SSE; no partial previews |
| Admission | one slot held for the whole render (typically 10 to 60 s) |
| Providers | OpenAI direct for the GPT Image models, never Azure, Bedrock or an Anthropic deployment |

## Response headers

| Header | When | Meaning |
|---|---|---|
| `x-request-id` | every call | the id `GET /v1/generation?id=` audits |
| `x-tm-provider` | served requests | deployment that rendered the image |
| `x-tm-attempts` | after ≥ 1 dispatch | physical dispatches, failovers included |
| `x-tm-upstream-status` | when a provider answered | the provider's own HTTP status |
| `x-tm-dropped-params` | when non-empty | comma-joined names of parameters RouterPlus stripped |
| `x-tm-upstream-model` | when the route knows the model by another id | the id RouterPlus sent |
| `x-tm-served-by` | when a route reports one | the provider that ran the request |
| `x-tm-route-plan-id` | billed requests | the route plan that chose the deployment |
| `x-tm-admission-mode`, `x-tm-remaining-rpm`, `x-tm-remaining-tpm` | billed requests | how the request was admitted and the headroom left. See [Limits and capacity](/docs/admission). |
| `x-tm-error-code` | every failure | the canonical error class |
| `x-tm-error-origin`, `x-tm-limit-scope`, `x-tm-limit-kind` | limit, provider and infrastructure failures | where it came from, and the limit that refused it |

There is no image-specific header. Browsers can read all of these except `x-tm-error-code`
and `x-tm-route-plan-id`, which are not CORS-exposed. For SSE, the `response_headers` extension
of the completed event also carries the final routing headers.

## Request data

Prompts, image bytes and any `revised_prompt` follow the [Data policy](/docs/data-policy).
Process logs, telemetry and the billing ledger contain metadata only. Telemetry records the
response byte size, to measure throughput.

## Model discovery

```bash
curl -s "https://demo-api.routerplus.com/v1/models?output_modalities=image" \
  -H "Authorization: Bearer $TM_API_KEY"
```

Every row of `GET /v1/models` carries `architecture.output_modalities`, for example
`["image"]`. The public feed `https://demo.routerplus.com/api/models.json` carries, for image models:

- `output_modalities`
- the full `supported_parameters` descriptor map
- `max_output_tokens`, the per-image ceiling

See [GET /v1/models](/docs/api-models) and [Models & catalog](/docs/models).

## BYOK

Image generation works through an `openai` provider connection.

- List on the connection the exact image ids you intend to send. Matching is literal. An id
  the connection does not carry is a `404 model_unavailable`.
- The model's metadata (ceiling, descriptors) still comes from the RouterPlus catalog.
- `usage.cost` is `0` and RouterPlus charges nothing. OpenAI bills your own account for the
  render.

See [Bring your own key](/docs/byok).

See also: [Models & catalog](/docs/models) · [Pricing & billing](/docs/pricing) ·
[Errors](/docs/errors) · [Wire compatibility](/docs/compat)
