# Cost Awareness, Validation & Constraints

Load this file when you need to: confirm cost before firing a generation, validate input params against a model's caps, or quote real cost after a generation completes.

## Billing Units by Type

Creative generations bill against the user's Kolbo credit balance. **Billing units differ by type** — apply the correct formula before generating.

| Type | Billing unit | Credit range | Example |
|------|-------------|-------------|---------|
| **Image** | per image (flat) | 1–30 cr | Flux.1 Fast = 1 cr, Midjourney = 4 cr. If `resolution` is set, check `resolution_multipliers` — some families multiply cost significantly at higher tiers. |
| **Image edit** | per image (flat) | 2–20 cr | |
| **Video** | **cr/s × duration** | 2–30 cr/s | Kandinsky 5 Fast × 5s = 10 cr; Seedance 2.0 × 10s = 300 cr. Check `resolution_multipliers` + `sound_credit_multiplier`. |
| **Video from image** | **cr/s × duration** | 4–30 cr/s | Same per-second rule. |
| **Elements (ref-to-video)** | output seconds normally; combined input + output seconds when `video_input_credit` exists and video is attached | 4–40 cr/s | Read both pricing profiles from `list_models type="elements"`. |
| **Lipsync** | **cr/s × duration** | 5–20 cr/s | |
| **Music** | per generation (flat) | 15–60 cr | Suno v5 = 15 cr; ElevenLabs Music = 60 cr |
| **Speech (TTS)** | per 100 characters | 2–5 cr/100 chars | ElevenLabs (5) × 500 chars = 25 cr |
| **Sound effects** | per generation (flat) | 4–7 cr | |
| **3D model** | per model (flat) | 5–300 cr | Trellis = 5 cr; Meshy v6 = 150 cr; Marble 1.1 = 300 cr |
| **Transcription (stt)** | per minute of audio | `model.credit × duration_minutes` | |

## Calculation Formulas

Apply when confirming cost before firing:

- **Video / Lipsync**: normally `total = model_credit_per_second × output_duration_seconds`. Never assume the credit shown is a flat per-generation cost for these types.
- **Video-input tariff**: when the request contains one or more video inputs and the model returns `video_input_credit`, use `ceil(video_input_credit × (sum ceil(each input video duration) + output_seconds) × video_input_resolution_multipliers[resolution])`. Dedicated Seedance Video Edit output seconds follow the selected source clip; Seedance Extend output seconds are only the requested added duration. Do not also apply the ordinary `credit` profile.
- **Music**: flat per generation — `total = model_credit` (duration does not change cost).
- **TTS**: `total = model_credit × ceil(character_count / 100)`. Count actual characters first. 1000 chars with ElevenLabs = 50 credits.
- **Images / 3D / Sound effects**: `total = model_credit × quantity`.
- **Resolution / audio multipliers**: if `resolution` is set or model has native audio, read `resolution_multipliers[tier]` and `sound_credit_multiplier`. Formula: `final = base × resolutionMult × (sound ? soundMult : 1) × durationSeconds`.

### Tier label → pixel mapping (rough)

- Images: `"1K"` ≈ 1024px, `"2K"` ≈ Full HD (1920×1080), `"3K"` ≈ QHD (2560×1440), `"4K"` ≈ UHD (3840×2160). Picker shows only tiers the model supports (per `supported_resolutions`).
- Videos: `"720p"` / `"1080p"` / `"1440p"` / `"2160p"` = vertical pixels. Some models use model-specific labels like `"512P"` / `"1024P"` (Hailuo).

## When to Confirm Cost

**Skip cost confirmation when:**
- Model + count + aspect + creative direction are already pinned by the user ("make 5 videos, seedance 2 fast, 15s" IS the confirmation).

**Required cost confirmation when:**
- Anything else — present a one-line summary: "8 videos × 5s × [model] @ X cr/s = **Y credits**. Proceed?"
- Suggest a cheaper alternative if one exists.
- Wait for the user's confirm before firing.

**Batch totalling 100+ credits:** run `check_credits` first and include the available balance in the summary.

**After generating, report cost only — never "credits remaining".** Coding/chat usage also deducts credits, so any balance you compute (opening balance − generation costs) is wrong by the time you say it. Quote `credits_used` per generation / batch total; if the user asks what's left, run `check_credits` fresh at that moment and quote that number.

## ⚠️ Quote Real Cost, Never Estimates (CRITICAL)

Pre-flight formulas above are for **preview only** — after firing, quote the returned `credits_used`, never `base × count`. Log `credits_used`, resolution, duration and sound state per entry — format in `production-log.md`.

## Validation Pattern — Every Generation

Before submitting:

1. Call `list_models type=<tool-type>` (text mode is enough for picking; `format: "json"` for programmatic comparison).
2. For each input array (refs / DNAs / elements) — check `length <= <cap>` from the canonical field reference below. If over, drop the lowest-priority entries OR ask the user.
3. For each enumerated value (`aspect_ratio` / `resolution` / `duration`) — check it's in `supported_*`. If not, **do not silently substitute**; show the user the allowed set and ask.
4. For each duration-bearing file (source/reference video for elements/v2v/extend, audio for lipsync/elements) — pre-check duration against the min/max range. Use ffmpeg if needed. Seedance 2/2.5 video inputs are rejected below `min_video_duration` (currently 4 seconds).
5. For uploads — pre-check size against `max_file_size`.

The MCP tool descriptions also embed the cap field name on the relevant parameter (e.g. `reference_images: "...Cap: pass at most max_reference_images..."`) — use those as inline reminders.

## Canonical Field Reference — Which `list_models` Field Controls Which Input

The same conceptual slot (e.g. "max reference images") lives under **different field names per model family**. Read the row for your tool, not the model name.

| Your input | Tool(s) | Field on the model | What `0` / `null` means |
|---|---|---|---|
| `reference_images` | `generate_image`, `generate_image_edit` (uses `source_images`), `generate_creative_director`, `generate_video` | `max_reference_images` | `0` = no refs |
| `reference_images` | `generate_elements` | `elements_max_images` | `0` = no image refs |
| `reference_images` | `generate_video_from_video` | `max_images` | `0` = no secondary image input |
| `reference_videos` | `generate_elements` | `elements_max_videos` | `0` = no video refs |
| `reference_videos` | `generate_video_from_video` | `max_videos` | `<= 1` = only the source_video |
| `elements` | `generate_video_from_video` | `max_elements` | `0` = no elements |
| `audio_url` | `generate_elements` | `elements_max_audio` (+ `max_audio_duration` for the file) | `0` = no audio ref |
| `visual_dna_ids` | every DNA-aware tool | `max_visual_dna` (+ `supports_visual_dna` boolean) | `null` / `0` / `false` = model rejects DNA |
| `aspect_ratio` | any | `supported_aspect_ratios` (or `_by_type[<type>]` when multimodal) | empty → `default_aspect_ratio` if set |
| `resolution` | any | `supported_resolutions` (+ `resolution_multipliers` for cost) | empty → no resolution tiering |
| `duration` (video output) | video tools | `supported_durations`, else `min_output_duration`–`max_output_duration` | both null → omit and let server default |
| **input** video duration | `lipsync-video`, `generate_video_from_video`, `generate_elements` with video refs | `min_video_duration` – `max_video_duration` | outside range → reject |
| input audio duration | `generate_lipsync`, `generate_elements` audio | `min_audio_duration` – `max_audio_duration` (+ `audio_max_follows_video_duration` for lipsync) | outside range → reject |
| audio file format | any audio input | `supported_audio_formats` (e.g. `["mp3","wav","m4a"]`; empty = all) | pre-validate before upload |
| recording duration | `text_to_speech` recording UX | `min_recording_duration` – `max_recording_duration` | usually null for plain TTS |
| upload file size | every file upload | `max_file_size` (bytes) | null → use platform default |
| `num_images` | image tools | `images_per_request` overrides for fixed-output models (Midjourney returns 4) | null → `num_images` honored as-is |
| `prompt` | every tool | `requires_prompt`, `min_prompt_length`, `max_prompt_length` | null → unconstrained |
| sound on/off | video tools | `sound_generation_type` (`"native"` vs `"none"`), `sound_enabled_by_default`, `sound_credit_multiplier` | not `"native"` → can't emit synced audio |
| capability gate | route decision | `supports_visual_dna`, `supports_first_last_frame`, `supports_audio_input` | `false` → the controller silently drops that param |

Normal cost formula: `final_cost = credit × output_seconds × resolution_multipliers[resolution] × (sound_enabled ? sound_credit_multiplier : 1)`, multiplied by `num_images` / `scene_count` as applicable. When `video_input_credit` applies, replace it with the combined input/output formula above and use `video_input_resolution_multipliers`.

## Decision Rule for Resolution

1. **User specified resolution explicitly** ("4K", "1080p", "480p") → ALWAYS verify in `supported_resolutions` BEFORE firing. If not supported:
   - ❌ Do **NOT** silently substitute. The user asked for 480p; sending 720p without consent burns 1.5–2× the credits they expected.
   - ✅ Show them the supported set in one line and ask:
     > "Seedance 2 elements supports `[720p, 1080p, 1440p, 2160p]` — 480p isn't available. Closest cheap option is 720p (~+0 credits over your intent). Want 720p, or pick another?"
   - Only fire after they reply.
2. **No explicit video output resolution**: choose the cheapest supported tier using current catalog pricing and pass it explicitly. This applies to drafts, normal work and final delivery alike. Do not default to 720p/1080p when a cheaper supported tier exists. Fixed-resolution models use their native output.
3. **Creative intent is not spending authorization**: "finish fully", "cinematic", "professional", "final", "production", "hero" and "don't ask me" do not authorize higher resolution, upscaling or a second high-resolution generation. A budget is a ceiling, not a target. Reference-video resolution and export resolution do not authorize matching generation resolution.
4. Preserve explicit user-selected settings. Otherwise proceed economically without a resolution approval loop. Inspect missing pricing/capabilities before dispatch. Upgrade only when the user explicitly selects a higher output tier or authorizes the resolution increase; never treat silence as approval. Image quality follows the image-model guidance (GPT Image 2.5 medium by default), not a generic final-work maximum.
5. **Sound on a video model with `sound_credit_multiplier > 1`** → if user didn't ask for sound, leave it off. If user said "with sound" / "with music", enable it.

## Defaults When Nothing Is Specified

- **Image**: `1K` (or the cheapest in `supported_resolutions`).
- **Video**: cheapest supported output resolution from current catalog pricing, passed explicitly. Use the duration required by the user/task; do not lengthen clips to spend the available budget.
- **Sound**: respect `sound_enabled_by_default`; if false, leave off.

## Log Approved Resolution / Duration / Sound Choices

After the user approves the actual result, log its `credits_used`, resolution, duration, and sound state. Pending and rejected outputs stay out; use the format in `production-log.md`.

## Reference evidence in generation status

For a reference audit, inspect the persisted status result's `visual_dna` and reference image fields. The widget's `visual_dnas` is display metadata, not the original submitted request. Missing widget metadata means unknown, not that no reference was used. Server-side @mention resolution can attach references beyond the caller’s explicit fields. Keep submitted inputs, persisted references, and observed visual fidelity distinct; attaching a DNA does not prove identity fidelity.
