> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aurous-labs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Aurous-Version date entries with diffs.

The Aurous Labs API uses **date-pinned versioning**. Send `Aurous-Version: 2026-05-15` (or any prior date) on every request to opt into a specific contract; we echo the applied version on every response. Missing the header falls back to your team's `default_api_version` (current at signup).

Breaking changes ship under a new date; non-breaking additions are listed under the current date.

## 2026-07-31 — Machine-readable `error_code`, honest interruption messaging, retryable `503`s on account/billing checks

A non-breaking batch that makes generation failures easier to handle programmatically: a machine-readable `error_code` on every generation, honest interruption messaging, and `503` responses with a `Retry-After` header when the platform can't momentarily verify your account or billing state.

* **New `error_code` field on every generation.** `POST`/`GET /v1/images` (create, detail, list — video rows included) and `POST /v1/videos` (create), plus webhook `data` payloads, now carry `error_code` — a machine-readable, closed-enum reason for `status: failed`: `generation_interrupted`, `reference_preparation_failed`, `content_filtered`, or `generation_failed`. Switch on `error_code`, not on `error_message` (human-readable prose that can change without notice). `null` on non-failed generations and on failures recorded before this field existed — always keep a default branch in your switch statement. Purely additive. Related transient failures that don't reach a terminal generation row instead surface as [`503 provider_unavailable`](/errors#provider_unavailable) — see that page for retry guidance.
* **Interrupted generations say so plainly.** A generation interrupted before completion that couldn't be recovered now reports `error_code: "generation_interrupted"` with `error_message: "The generation was interrupted before it could complete and could not be recovered. Credits have been refunded."` — and the refund is guaranteed on this path.
* **Account/billing checks now answer `503` with a `Retry-After` header when the platform can't momentarily verify your account or billing state.** Any V1 endpoint that checks that state — including `POST /v1/images`, `POST /v1/videos`, their `/estimate` counterparts, and `GET /v1/team` / `/v1/usage` / `/v1/balance` / `/v1/loras` — returns `503 provider_unavailable` with a `Retry-After` header (typically `5`). See [`provider_unavailable`](/errors#provider_unavailable).
* **`503` responses are retry-safe under `Idempotency-Key`.** A `503` is never cached against your key — retry the exact same `Idempotency-Key` and it re-executes normally; you will not see `409 idempotency_key_in_use` as a result of a `503`. See [Idempotency](/idempotency).
* No action required beyond honoring `Retry-After` on `503` and preferring `error_code` over `error_message` going forward.

## 2026-07-24 — Styles are back: prompt styles, auto-matching with opt-out, compose with acts

Styles return to `POST /v1/images` as prompt styles — richer look control on the current image models — with automatic matching, full composability with composition acts and subjects, and honest signaling (`style` echo + `warnings[]`) so the applied outcome is always observable. Batch `count` now works on every request shape. Existing integrations keep working; the behavior notes below are worth reading if you pin `lora_id` or key off estimate quotes today.

* **`lora_id` is now tri-state.** Omit it and style matching runs automatically when your prompt clearly names a look (conservative — most prompts resolve to no style); send `null` to disable matching for a request; send an id (or slug) from [`GET /v1/loras`](/api-reference/openapi) to pin a style. See [Create an image → Using a style](/api-reference/images/create-image#using-a-style).
* **Every generation echoes `style: { id, name }`** (or `null`) — on creates, GETs, list rows, and webhook `data` payloads. `style.id` is the same `lora_*` identifier the catalog serves and round-trips into `lora_id`. A new, always-present, nullable key: integrations that don't read it are unaffected.
* **Styles compose with acts and subjects.** `lora_id` + `action_id` and `lora_id` + `subjects[]` are now valid combinations (previously `400 parameter_invalid_combination`). One rule to know: a composition-act id sent as `lora_id` acts as the pin itself, so it can't be combined with a *different* `action_id`. `lora_id` + `context_images` remains invalid.
* **Retired style ids keep working.** An id with a designated successor applies the successor style (echoed in `style`); other retired ids generate without a style and add a `warnings[]` entry (`style_retired_plain`). A small set of discontinued styles return the new [`400 style_retired`](/errors#style_retired) — on create *and* estimate.
* **New `warnings[]` on create + estimate responses.** Non-fatal request adjustments — a retired style pin, or a parameter with no effect on the selected generation path (`parameter_ignored`). Omitted when empty; never on GETs, lists, or webhooks; the code set is open, so ignore unknown codes. Idempotent replays return the original warnings verbatim.
* **`count` works everywhere, in parallel.** Every request shape — plain, `subjects[]`, `context_images` — now delivers up to 4 images per request, billed per image; if part of a batch fails you receive the successful images and the difference is refunded automatically (`image_count` reflects delivered). Estimates quote `count ×` accordingly: `subjects`/`context_images` estimates previously quoted a single image and now quote the full batch.
* **`count` must be a whole number.** `count: 3.5` now returns a validation `400` (it was previously accepted and rounded down).
* **Behavior change:** with `lora_id` omitted, a prompt that clearly names a look may now generate with a matched style where it previously always generated plain. The `style` echo makes this visible; send `lora_id: null` if you need guaranteed-unstyled output. Applies across all version pins.
* **Behavior change:** styled generations run on the current image models, which serve prompt + size and ignore the legacy sampler fields; a sampler field sent on such a request is reported via `warnings[]` rather than silently dropped.

## 2026-07-22 — Multi-person video casts, reference-media uploads, and a contract-closure batch

One breaking removal (a webhook field with zero recorded uses) plus a set of additive capabilities and precision fixes that close the drift between the documented contract and live behavior.

* **Breaking: the per-request `webhook_url` field is removed** from `POST /v1/images` and `POST /v1/videos`. It never delivered what the docs promised (wrong event names, an internal id, an unsanitized field subset) and has **zero recorded uses across all teams, all-time** — which is why it is removed outright rather than gated behind a new `Aurous-Version` date. Requests that still send it receive `400 parameter_unknown` under every version pin. Use [registered webhook endpoints](/webhooks) instead (`POST /v1/webhook_endpoints`): the full event catalog (`image.*` and `video.*` including `.cancelled`, `character.*`, balance events), signed payloads, delivery logs, and retries.
* **Cast up to 2 people in a video** via `subjects[]` on [`POST /v1/videos`](/api-reference/videos/create-video) — each entry a saved character (`char_<ulid>`) or its own group of 1–4 reference images (`file_<ulid>` or HTTPS URLs), freely mixed; the same wire grammar the images surface uses. A cast may ride a `reference_video_url` — the clip supplies the motion while the cast anchors identity. Casts don't change the price. The legacy single-subject fields (`character_id`, `reference_image_urls`) remain fully supported and now carry the advisory `Deprecation: true` header on the video routes too.
* **Upload reference media.** [`POST /v1/files`](/api-reference/openapi) accepts `purpose: reference_video` (MP4/MOV, ≤50 MB) and `purpose: reference_audio` (WAV/MP3, ≤15 MB); `reference_video_url` / `reference_audio_url` accept the returned `file_<ulid>` anywhere an HTTPS URL was accepted. Format, duration, and resolution validate at upload time — an accepted upload can never fail those checks at create. `FileResponse` gains `duration_s` (parsed clip duration; `null` for images).
* **Audio references pair with more companions.** `reference_audio_url` now anchors to a reference video, a subject, or a pinned model — previously it required a reference video.
* **Video generations echo their cast.** `subjects[]` appears on video generation responses with the same ordered, opaque projection images use: `[{type, character_id, image_count}]` — never URLs, never internal ids. `null` on generations without a cast.
* **`enhancer_outcome` joins the documented contract** — the informational field behind "Enhancer declined this prompt — your raw text was used." Always present, nullable. Treat it as an open set: new values may be added without a version bump, so don't branch control flow on it.
* **`GET /v1/usage` speaks `succeeded`.** Status facet values now use the same public vocabulary as the generations surface (previously the internal `completed` spelling leaked through). The `status` filter accepts both spellings.
* **List / webhook / detail parity fixes.** `GET /v1/images` list rows and webhook `data` payloads now carry the full documented `GenerationResponse` shape (`negative_prompt`, `cfg_rescale`, `denoise_strength`, `seed`, `aurous_version`, `creation_request_id` had drifted off some surfaces), and the generation `status` enum is defensively frozen to its 5 documented values.

## 2026-07-18 — Raw Seedance video API + per-model, token-based video pricing

Two video additions ship together: a provider-native "raw" Seedance surface for teams that already build against the Seedance SDK, and per-model, token-based video pricing so the rate you read is the rate you pay. Both are additive — existing `POST /v1/videos` integrations are unaffected.

* **Raw Seedance task API.** New `POST /v1/contents/generations/tasks`, `GET /v1/contents/generations/tasks/{id}`, `GET /v1/contents/generations/tasks`, and `DELETE /v1/contents/generations/tasks/{id}`. The request and task bodies are byte-exact to the native Seedance video API — Aurous forwards your Ark-SDK payload verbatim, including fields not yet documented here, wrapped in the same credit holds, `Idempotency-Key` support, rate limits, and typed [error envelope](/errors) as the rest of the V1 API. Point your existing Seedance SDK client at Aurous and go — see [raw Seedance access](/api-reference/seedance/overview).
* **Video generation is priced per model on your actual token usage.** Each video model carries its own token rate, and you are billed for the tokens a generation actually consumes. Published rates exactly match what you are charged: a ceiling is held while the task runs, and the final charge settles on the provider's reported tokens at success — a `failed`, `expired`, or queued-`cancelled` task charges nothing and releases the hold in full. Read the live rate for any model from [`GET /v1/models`](/api-reference/openapi#tag/models); the day-one rate card and the formula behind it are on [Seedance models & pricing](/api-reference/seedance/models-and-pricing).
* **Two new lower-cost tiers.** `seedance-2.0-fast` and `seedance-2.0-mini` join the full-quality `seedance-2.0`, giving you cheaper options when you don't need the flagship model. Address any model by id or alias.
* **Video-input tasks meter reference-video seconds.** A task whose `content[]` includes a reference video is billed for the reference footage it ingests in addition to the video it generates, so the receipt reflects the full cost of the work.
* **No version bump.** Video rates are per-model and DB-driven — they can change without a new `Aurous-Version`, the same way chat and embedding rates do. Always read the current rate from `GET /v1/models` rather than hard-coding it; see the [rate-mutability asymmetry](/api-reference/chat/pricing#mutability-asymmetry-vs-images-and-videos). Each charged task snapshots the rate in force when it was created, so the amount you were quoted is the amount you pay.

## 2026-07-16 — Reference video/audio on `POST /v1/videos`

`POST /v1/videos` accepts a third input mode alongside text and frames: `reference_video_url`, an HTTPS URL to your own clip. The generation either borrows the clip's motion for a new scene (`video_task: "reference"`, the default) or continues the clip itself (`video_task: "extend"`). Optionally pair it with `reference_audio_url` to guide the generated soundtrack. This is additive — existing text-to-video and image-to-video requests are unaffected.

* **New request fields on `POST /v1/videos` and `POST /v1/videos/estimate`.** `reference_video_url`, `reference_audio_url`, `video_task` (`reference` | `extend`), `extend_direction` (`forward` | `backward`). See the endpoint reference for size/duration/format limits and the full parameter-combination rules.
* **New response fields.** `video_task`, `extend_direction`, `reference_video_url`, `reference_audio_url` — `null` on every generation that did not use this mode, including all pre-existing rows.
* **`inference_type` gains `r2v`** (reference-to-video). A generation that used your own `reference_video_url` reports `inference_type: "r2v"`; frame-driven, subject-driven, and pinned Action/template videos continue to report `i2v`. Treat unrecognized `inference_type` values as opaque.
* **New error codes.** `reference_fetch_failed` (couldn't fetch your reference URL) and `reference_media_invalid` (fetched, but failed format/duration/resolution validation) — see [Errors](/errors).
* **Timing.** The reference is prepared inside `processing` — typically well under a minute, with a 10-minute budget before the generation fails with a full refund. A reused reference (identical bytes) skips preparation. If preparation fails, retry with a **new** `Idempotency-Key`.

## 2026-07-16 — Subject-driven video, a video model catalog, and adaptive duration by default

`POST /v1/videos` now has the same generation surface as `POST /v1/images`: pin or auto-match a video model, drive a video from a saved character or reference images, and price adaptive-length videos honestly. All changes are additive — existing integrations keep working — with two behavior notes below worth reading if you key off `video_lora_id` or `inference_type` today.

* **`POST /v1/videos`.** Accepts `character_id` and `reference_image_urls` (up to 6), mirroring the same fields on `POST /v1/images` — the subject is anchored across the generated video. Mutually exclusive with each other (`400 mutually_exclusive_input`) and with `first_frame_url` / `last_frame_url` (`400 parameter_invalid_combination`). See [Create a video](/api-reference/videos/create-video).
* **`GET /v1/video_loras` now lists real, pinnable video models** (previously effectively empty). Pin one with `video_lora_id`, or omit it and the platform matches your prompt to a suitable model automatically. Whichever model ends up behind a generation — pinned or matched — is echoed back on the response as `video_lora_id` and `video_lora_name`.
* **Adaptive duration is the default.** Omit `duration` (or pass `-1`) and the model picks the natural length for your prompt, 4–15 seconds, and you're billed for the length actually delivered — not a flat ceiling. `POST /v1/videos/estimate` prices this as a range (`amount_min`–`amount_max`); the ceiling is reserved up front and the difference is released once the real length is known. Pin a whole-number duration (4–15) for a fixed length at a fixed price.
* **`cost.refunded` is a new field.** A failed generation now reports `cost: {"amount": 0, "refunded": true}` instead of a bare zero, so you can tell "never charged" apart from "charged nothing this time." Present on both image and video generations.
* **Behavior change:** a prompt-only video request that previously always rendered as plain text-to-video may now be auto-matched to a video model, producing a different look than before. Pin `video_lora_id` explicitly if you need the exact prior output character.
* **Behavior change:** a subject-driven video (`character_id` or `reference_image_urls`) reports `inference_type: "i2v"` on the generation response — the same value used for first/last-frame videos. There is no separate subject-specific value.

## 2026-06-21 — Transient storage failures now return `503` + `Retry-After`

Upload and character-create routes that touch object storage now surface a transient storage failure as a retryable `503 provider_unavailable` carrying a `Retry-After` header, instead of a generic `500`. This is a non-breaking improvement — no action required beyond honoring the new retry signal.

* **`POST /v1/characters/uploads/init`, `POST /v1/characters/uploads/classify`, and `POST /v1/characters`.** A transient storage error now returns `503 provider_unavailable` with a `Retry-After` header instead of a generic `500`. Retry on `503` after the number of seconds in `Retry-After`. Applies across all version pins.

## 2026-06-21 — Character synthesis rate limit raised to 15/min

The per-team rate limit on character synthesis increased from **6 requests/minute (burst 12)** to **15 requests/minute (burst 30)**. This is a loosening — no action required.

* **`POST /v1/characters`, `POST /v1/characters/:id/refs/regenerate`, `POST /v1/characters/:id/resynthesize`.** These three routes **share one per-team bucket**, now 15/minute sustained with a burst of 30. Each create or resynthesize fans out to four image generations internally, so the bucket is sized to match the `POST /v1/images` budget — 15 × 4 = 60 image generations/minute. A burst of regenerations or resynthesizes draws down the same budget as new creates.
* **Rate limits are not version-pinned.** The live `X-RateLimit-Limit` response header is always the authoritative current value; we raise limits without notice and announce any reduction in advance here. Applies across all version pins.

## 2026-06-19 — Character synthesis is now asynchronous + character webhook events

`POST /v1/characters` with `generate: true` no longer blocks until the character is rendered. It returns immediately with `status: "synthesizing"`; the synthesis runs in the background and the character transitions to `ready` (or `failed`). Poll `GET /v1/characters/:id`, or subscribe to the new character webhook events.

* **`POST /v1/characters`.** Now responds `201` with `status: "synthesizing"` instead of returning the finished character. An optional `client_reference_id` (≤ 256 chars) is echoed on the response and on every character webhook. **Behavior change:** callers that relied on the synchronous response must switch to polling `GET /v1/characters/:id` until `status` is `ready` or `failed`, or subscribe to webhooks. Applies across all version pins.
* **New webhook events.** `character.completed` (reached `ready`; payload carries the reference renders), `character.failed` (provider error, moderation, or timeout — `error_message` carries a customer-safe code), and `character.cancelled` (`DELETE /v1/characters/:id` on an in-flight synthesis). Subscribe via the `events` array on a webhook endpoint, or in the dashboard webhook settings.

## 2026-06-03 — Documented platform defaults now applied to omitted generation params

`POST /v1/images` now applies the platform default for `steps`, `guidance_scale`, `cfg_rescale`, and `denoise_strength` when you omit the parameter **and** the selected style does not define its own default. Previously these defaults were documented on the request fields but only enforced by the dashboard, so an API caller who omitted them fell through to the provider's raw default.

* **`POST /v1/images`.** Omitting `cfg_rescale` resolves to `0.7`, `steps` to `11`, and `guidance_scale` to `4.3`. Omitting `denoise_strength` on a request that has references resolves to `0.6` (it remains ignored on bare text-to-image requests). Precedence is unchanged: an explicit request value wins, then the selected style's default, then the platform default.
* **No request or response shape change.** No fields added or removed; the response continues to echo only the values you supplied — omitted params are resolved server-side and not reflected back.

This aligns behavior with the already-published `default` values on the request fields, so it applies across all version pins rather than shipping under a new date. The only observable change is for a caller who omitted `cfg_rescale` and relied on the provider's raw default. Zero customers were affected at release time.

## 2026-05-24 — Embedding video rate removed; `video_url` input rejected

The `embedding_pricing.video` field on `GET /v1/models` has been removed. Embedding billing has always folded `video_url` input into the visual bucket — the provider extracts frames from videos and bills them as visual tokens. The previously published `embedding_pricing.video` rate never actually fired. To prevent the contract from misleading integrators, the field is now gone.

* **`GET /v1/models`.** The `embedding_pricing` object on embedding-kind rows no longer contains a `video` key. `text` and `visual` are unchanged. The `visual` field description now notes explicitly that `image_url` parts bill at this rate; `video_url` input is no longer accepted (see below).
* **`POST /v1/embeddings` + `POST /v1/embeddings/estimate`.** Any `video_url` content part returns `400 embeddings_video_unsupported` (new error code). To embed visual content from a video, extract a representative frame in your pipeline and submit it as `image_url`; it bills at the visual rate.
* **Renamed error code.** `embeddings_video_too_many_parts` (which previously fired for ≥ 2 video parts) was renamed to `embeddings_video_unsupported` and now fires for ≥ 1 video part. Update any integration that caught the old code on a single-video payload.
* **Response shape compatibility.** `POST /v1/embeddings/estimate` retains `tokens.video` and `breakdown.input.video` on the response shape, both always `0`, for one release cycle so existing SDKs that read them don't break. Both fields will be dropped in a follow-up release.

This is a breaking change for any caller that pinned the `embedding_pricing.video` field or sent `video_url` parts. Embedding rates are explicitly mutable per the rate-card mutability note on `/v1/models` and `/v1/chat/completions`, so the change applies across all version pins.

Zero customers were affected at release time.

## 2026-05-11

* **Image size presets reshaped.** `size` now follows `<tier>_<ratio>` form: `2k_1_1`, `2k_3_2`, `2k_2_3`, `2k_4_3`, `2k_3_4`, `2k_16_9`, `2k_9_16`, `2k_21_9`, plus the same eight ratios at `4k_*`. Sixteen presets total. The previous six names (`square`, `landscape`, `portrait`, `hd_square`, `hd_landscape`, `hd_portrait`) return `400 invalid_format`.
* **Custom dimensions.** `width` and `height` now require both, in `[1024, 4096]` per side, and are snapped server-side to multiples of 32. The response `width`/`height` reflect the post-snap value (may differ from the request by up to 31 px per side).
* **Mutual exclusion.** Sending both `size` and `width`/`height` returns `400 parameter_invalid_combination`. Sending only one of `width`/`height` returns `400 missing_field`.

## 2026-05-15 (v1.0)

The first stable date-pin. Everything documented elsewhere on this site is what `2026-05-15` means.

### Resource shape

* `/v1/images` and `/v1/videos` are the create + read paths for image and video generations.
* `/v1/files` is the upload path for reference images you reuse across generations.
* `/v1/webhook_endpoints` registers + manages signed webhook deliveries.
* `/v1/loras` and `/v1/video_loras` are the public catalog of styles.
* `/v1/team`, `/v1/balance`, `/v1/usage`, `/v1/usage/events` cover account and metering. Per-model `chat_pricing` / `embedding_pricing` ships on `GET /v1/models` and reflects the caller's effective rate including any per-team overrides.

### Pricing

* Image cost = `image.base × count` plus an optional `enhance` line when `enhance_prompt: true`. Refs and `character_id` are free — neither shifts the price.
* Video cost = `base_per_second × resolution_factor × duration_s` (multiplicative). The breakdown carries the inputs; `cost.amount` is the product.
* Team-level discounts surface as a `discount_factor` line in the breakdown when the team has a negotiated rate.

### Webhook event types — v1.0 lineup

v1.0 ships eight event types. Four additional event types are reserved for a later date-pin — the underlying status transitions aren't yet wired into the production code path, so they're documented but never fire today.

**Currently fired:**

* `image.completed` / `image.failed` / `image.cancelled`
* `video.completed` / `video.failed` / `video.cancelled`
* `usage.balance_low` (per-team threshold; 1-hour debounce)
* `webhook.endpoint_disabled` (auto-disable notification)

**Reserved for a later date-pin (NOT fired today):**

* `image.expired` / `video.expired` — fires when a generation exceeds the 5-minute polling window without a terminal status. Requires a polling-timeout sweeper that flips `status` from `processing` to `expired`. Until then, stuck generations remain `processing`; the worker eventually fails them or the cancel endpoint resolves the hold.
* `image.moderation_rejected` / `video.moderation_rejected` — moderation rejection currently throws `400 invalid_request` from the create endpoint without inserting an `inferences` row, so there is nothing to fire a webhook against. Future change: insert the row in `moderation_rejected` state and fire the event.

If you subscribe with `events: ["*"]`, the snapshot taken at registration time includes only the events that exist when you register. A future date-pin that adds `*.expired` or `*.moderation_rejected` will require you to update your registration explicitly — `["*"]` does NOT auto-subscribe across date-pins.

### Status enum

Generation `status` is one of: `pending | processing | succeeded | failed | cancelled`. `expired` and `moderation_rejected` are reserved enum values per the spec but never fire as wire values in `2026-05-15` (see "Webhook event types" above for the same reason).

### Usage events — fields reserved for a later date-pin

`GET /v1/usage/events` returns line items with `id`, `type`, `amount`, `currency`, `generation_id`, `hold_id`, `description`, and `created_at`. Two fields commonly seen in similar ledger APIs (Stripe `events`, etc.) are intentionally NOT in `2026-05-15`:

* `balance_before` / `balance_after` — the team's credit balance immediately before and after the event. Computing these accurately requires an authoritative balance-tracking column on `usage_events` (or a transactional bookkeeping query at write time). Reserved for a follow-up date-pin once the bookkeeping column is added.

If you need a per-event running balance today, sum `amount` across rows in chronological order with the `type`-derived sign (`hold` / `charge` / `adjustment` with negative `description` debit; `release` / `refund` / `topup` / positive `adjustment` credit).

## 2026-05-22

LLM pricing transparency redesign. Three breaking changes, one removal, one units change.

### Model rename (breaking)

Four customer-facing slugs were renamed. Old slugs return `404 model_not_found`.

| Old slug              | New slug                  |
| --------------------- | ------------------------- |
| `aurous-chat-pro`     | `aurous-grow-2.0-pro`     |
| `aurous-embed-vision` | `aurous-embed-vision-1.0` |

Three Grow 2.0 chat models (`aurous-grow-2.0-mini`, `aurous-grow-2.0-lite`, `aurous-grow-2.0-code`) are unchanged but now share the family-wide `50%` markup (previously `15%`). No call-site change required.

### `/v1/pricing` removed (breaking)

`GET /v1/pricing` now returns `404`. Per-model rates are embedded directly in `GET /v1/models` under `chat_pricing` (chat models) and `embedding_pricing` (embedding models). One less round-trip; the data is identical and now lives alongside model metadata.

### `/v1/models` response shape (breaking)

The model response gained two nullable siblings — `chat_pricing` and `embedding_pricing` — replacing the previous `aurous_metadata.credits_per_1k_*` fields. Per founder direction the unit is now **per million tokens** (`credits_per_M`), not per 1K.

**Important — 1000× scale change.** Customers who hardcoded the previous `credits_per_1k_*` values into FinOps spreadsheets, monitoring dashboards, or rate-card exports must either (a) divide `credits_per_M` by 1000 to recover the per-1K number, or (b) update math to use per-million directly.

New response shape:

```json theme={null}
{
  "id": "aurous-grow-2.0-pro",
  "kind": "chat",
  "display_name": "Aurous Grow 2.0 Pro",
  "chat_pricing": {
    "input":  { "credits_per_M": 75  },
    "output": { "credits_per_M": 450 }
  },
  "embedding_pricing": null
}
```

For embedding models, `chat_pricing` is `null` and `embedding_pricing` carries `text` / `visual` / `video` per-modality rates.

### 60-second `jq` recipe — migrate a FinOps script

If you were pulling `aurous_metadata.credits_per_1k_input_tokens` from `/v1/models`, the equivalent value lives at `chat_pricing.input.credits_per_M / 1000`:

```bash theme={null}
curl -s -H "X-Api-Key: $AUROUS_KEY" https://api.aurous-labs.com/v1/models \
  | jq '.data
        | map(select(.kind == "chat"))
        | map({
            slug: .id,
            input_credits_per_1k:  (.chat_pricing.input.credits_per_M  / 1000),
            output_credits_per_1k: (.chat_pricing.output.credits_per_M / 1000)
          })'
```

For embedding models, swap `chat_pricing.input/output` for `embedding_pricing.text/visual/video`.

### What didn't change

* Receipt shape on `/v1/chat/completions` and `/v1/embeddings` responses (`usage`, `credits_charged`, `breakdown`) is unchanged.
* `pricing_version` in the receipt continues to snapshot the effective rate at hold time, so replays remain deterministic.
* Image and video pricing on `/v1/images`, `/v1/videos`, `/v1/loras`, `/v1/video_loras` is unaffected by this release.
* `Aurous-Version` pinning continues to work; chat and embedding rates intentionally track the most-recently published rate version (per spec — LLM provider economics shift on weekly cadence).
