openapi: 3.1.0
info:
  title: LLM Gateway API
  version: 1.0.0
  summary: OpenAI-compatible inference gateway with multi-provider routing, fallback, streaming and async media jobs.
  description: |
    The gateway exposes an OpenAI-compatible `/v1` API. Every request is authenticated with an
    API key (`Authorization: Bearer sk-llm-…`), routed across providers according to a routing
    strategy, and priced in integer micro-USD (1,000,000 micro-USD = $1).

    Platform extensions on top of the OpenAI wire format:

    * `routing` request object (`strategy`, `providers`) — stripped before forwarding.
    * `provider` on every response and stream chunk; `usage.x_llm_cost_micro` (charged amount).
    * Response headers `X-Request-Id`, `X-LLM-Provider`, `X-LLM-Model`, `X-LLM-Routing-Reason`, `X-LLM-Cost-Micro`.
    * A single error envelope with a stable `code` enum.
    * Asynchronous video and music jobs (`202 Accepted`, polling, cancel, signed webhooks) that require `Idempotency-Key`.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  contact:
    name: Platform team
servers:
  - url: '{gatewayUrl}'
    description: Gateway base URL (local default shown; substitute your deployment).
    variables:
      gatewayUrl:
        default: http://localhost:4300
security:
  - bearerAuth: []
tags:
  - name: Models
    description: Catalog of models available to your key.
  - name: Text
    description: Chat completions, legacy completions, Responses (subset) and embeddings.
  - name: Images
    description: Image generation and edits.
  - name: Audio
    description: Speech synthesis and transcription.
  - name: Video
    description: Asynchronous video generation jobs.
  - name: Music
    description: Asynchronous music generation jobs.
  - name: Jobs
    description: Kind-agnostic job lookup.
  - name: Ops
    description: Health endpoints (unauthenticated).

paths:
  /v1/models:
    get:
      tags: [Models]
      operationId: listModels
      summary: List models
      description: Returns models visible to the API key (filtered by `allowed_models` / `allowed_modalities`) with pricing, discount, capabilities and provider counts.
      responses:
        '200':
          description: Model list.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelList' }
              examples:
                default:
                  value:
                    object: list
                    data:
                      - id: openai/gpt-4o-mini
                        object: model
                        created: 0
                        owned_by: openai
                        display_name: GPT-4o mini
                        modality: text
                        input_modalities: [text, image]
                        output_modalities: [text]
                        context_window: 128000
                        max_output_tokens: 16384
                        pricing: { input_per_mtok: 150000, output_per_mtok: 600000, currency: USD, unit: micro }
                        discount_bps: 0
                        capabilities: { streaming: true, tools: true, vision: true }
                        supported_parameters: [temperature, top_p, max_tokens, tools, response_format, seed]
                        providers: 2
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /v1/models/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Model id in `vendor/name` form. The slash may be percent-encoded.
        schema: { type: string, examples: ['openai/gpt-4o-mini'] }
    get:
      tags: [Models]
      operationId: getModel
      summary: Get a model
      responses:
        '200':
          description: Model.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Model' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /v1/chat/completions:
    post:
      tags: [Text]
      operationId: createChatCompletion
      summary: Create a chat completion
      description: |
        OpenAI Chat Completions request. Set `stream: true` for Server-Sent Events; the stream always
        ends with a usage chunk (empty `choices`, `usage` present) followed by `data: [DONE]`.
        Errors after the first token are emitted as `data: {"error": {...}}` then `[DONE]`.

        Example stream (each `data:` payload is a `ChatCompletionChunk`; `: ping` comments may appear while waiting):

        ```
        data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1725446400,"model":"openai/gpt-4o-mini","provider":"openai","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

        data: {"id":"chatcmpl-1","object":"chat.completion.chunk","created":1725446400,"model":"openai/gpt-4o-mini","provider":"openai","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":1,"total_tokens":13,"x_llm_cost_micro":2}}

        data: [DONE]
        ```
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ChatCompletionRequest' }
            examples:
              simple:
                value:
                  model: openai/gpt-4o-mini
                  messages: [{ role: user, content: Say hello in one sentence. }]
              routing:
                summary: With routing override
                value:
                  model: meta/llama-3.1-70b-instruct
                  messages: [{ role: user, content: Summarise this text. }]
                  routing: { strategy: fastest, providers: [groq, fireworks] }
              streaming:
                value:
                  model: openai/gpt-4o-mini
                  stream: true
                  messages: [{ role: user, content: Count to five. }]
      responses:
        '200':
          description: Completion (JSON) or event stream when `stream` is true.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChatCompletionResponse' }
              examples:
                default:
                  value:
                    id: chatcmpl-8f1c
                    object: chat.completion
                    created: 1725446400
                    model: openai/gpt-4o-mini
                    provider: openai
                    choices: [{ index: 0, message: { role: assistant, content: Hello! }, finish_reason: stop }]
                    usage: { prompt_tokens: 12, completion_tokens: 3, total_tokens: 15, x_llm_cost_micro: 4 }
            text/event-stream:
              schema: { $ref: '#/components/schemas/ChatCompletionChunk' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/completions:
    post:
      tags: [Text]
      operationId: createCompletion
      summary: Create a completion (legacy)
      description: Legacy prompt API. Mapped to chat completions internally for providers that lack a native completions endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CompletionRequest' }
      responses:
        '200':
          description: Completion or event stream.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CompletionResponse' }
            text/event-stream:
              schema: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/responses:
    post:
      tags: [Text]
      operationId: createResponse
      summary: Create a response (partial Responses API)
      description: |
        **Partial implementation.** Supported fields: `model`, `input` (string or message array), `instructions`,
        `max_output_tokens`, `temperature`, `stream`, `tools`, `routing`. Mapped to chat completions internally.
        Streaming emits Responses-style events (`response.output_text.delta`, …) and ends with `data: [DONE]`.
        Stateful features (`previous_response_id`, built-in tools, background mode) are not supported.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ResponsesRequest' }
            example:
              model: openai/gpt-4o-mini
              instructions: Answer in one word.
              input: What colour is the sky on a clear day?
      responses:
        '200':
          description: Response object or event stream.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ResponsesResponse' }
            text/event-stream:
              schema: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/embeddings:
    post:
      tags: [Text]
      operationId: createEmbedding
      summary: Create embeddings
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmbeddingRequest' }
            example: { model: openai/text-embedding-3-small, input: [The quick brown fox, jumps over the lazy dog] }
      responses:
        '200':
          description: Embeddings.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmbeddingResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/images/generations:
    post:
      tags: [Images]
      operationId: createImage
      summary: Generate images
      description: When object storage is configured, `url` values are signed URLs (default validity about 1 hour); otherwise the provider URL is passed through. Supports `Idempotency-Key`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ImageGenerationRequest' }
            example: { model: openai/dall-e-3, prompt: An isometric data centre at dusk, n: 1, size: 1024x1024, response_format: url }
      responses:
        '200':
          description: 'Generated images. `Idempotent-Replayed: true` when served from the idempotency store.'
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
            Idempotent-Replayed: { $ref: '#/components/headers/Idempotent-Replayed' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImageResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/images/edits:
    post:
      tags: [Images]
      operationId: createImageEdit
      summary: Edit an image
      description: Accepts `multipart/form-data` (`image`, `mask?`, `prompt`, `model`, `n`, `size`) or JSON with `input_images[]` URLs / base64. Supports `Idempotency-Key`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: '#/components/schemas/ImageEditMultipart' }
          application/json:
            schema: { $ref: '#/components/schemas/ImageGenerationRequest' }
      responses:
        '200':
          description: Edited images.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
            Idempotent-Replayed: { $ref: '#/components/headers/Idempotent-Replayed' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImageResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/audio/speech:
    post:
      tags: [Audio]
      operationId: createSpeech
      summary: Generate speech
      description: Returns audio bytes as a chunked stream (no buffering). Retry/fallback only before the first byte.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SpeechRequest' }
            example: { model: openai/tts-1, input: Hello from the gateway., voice: alloy, response_format: mp3 }
      responses:
        '200':
          description: Audio stream. Content-Type matches `response_format`.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
          content:
            audio/mpeg: { schema: { type: string, format: binary } }
            audio/ogg: { schema: { type: string, format: binary } }
            audio/aac: { schema: { type: string, format: binary } }
            audio/flac: { schema: { type: string, format: binary } }
            audio/wav: { schema: { type: string, format: binary } }
            audio/pcm: { schema: { type: string, format: binary } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/audio/transcriptions:
    post:
      tags: [Audio]
      operationId: createTranscription
      summary: Transcribe audio
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: '#/components/schemas/TranscriptionMultipart' }
      responses:
        '200':
          description: Transcription.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-LLM-Provider: { $ref: '#/components/headers/X-LLM-Provider' }
            X-LLM-Model: { $ref: '#/components/headers/X-LLM-Model' }
            X-LLM-Routing-Reason: { $ref: '#/components/headers/X-LLM-Routing-Reason' }
            X-LLM-Cost-Micro: { $ref: '#/components/headers/X-LLM-Cost-Micro' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TranscriptionResponse' }
              example: { text: Welcome everyone, let's get started. }
            text/plain:
              schema: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '415': { $ref: '#/components/responses/UnsupportedMediaType' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '502': { $ref: '#/components/responses/ProviderError' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }
        '504': { $ref: '#/components/responses/ProviderTimeout' }

  /v1/video/generations:
    post:
      tags: [Video]
      operationId: createVideoGeneration
      summary: Submit a video generation job
      description: |
        Asynchronous. Returns `202 Accepted` with the job in `queued` state. `Idempotency-Key` is **required**
        (`400 idempotency_key_required` otherwise); replaying the same key + body returns the existing job with
        `Idempotent-Replayed: true`. Poll `GET /v1/video/generations/{id}` or supply `webhook_url`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VideoGenerationRequest' }
            example:
              model: vendor/video-model
              prompt: A slow aerial shot over a foggy pine forest at sunrise
              duration_seconds: 8
              aspect_ratio: '16:9'
              webhook_url: https://api.example.com/hooks/llm-jobs
      responses:
        '202': { $ref: '#/components/responses/JobAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '501': { $ref: '#/components/responses/NotImplemented' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }

  /v1/video/generations/{id}:
    parameters:
      - $ref: '#/components/parameters/JobId'
    get:
      tags: [Video]
      operationId: getVideoGeneration
      summary: Get a video job
      responses:
        '200': { $ref: '#/components/responses/Job' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
    delete:
      tags: [Video]
      operationId: cancelVideoGeneration
      summary: Cancel or delete a video job
      description: Non-terminal jobs are cancelled (→ `canceled`, reservation released) and returned. Terminal jobs are deleted along with their artifacts.
      responses:
        '200': { $ref: '#/components/responses/JobCanceled' }
        '204': { description: Terminal job and its artifacts deleted. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /v1/music/generations:
    post:
      tags: [Music]
      operationId: createMusicGeneration
      summary: Submit a music generation job
      description: Asynchronous, same semantics as video. `Idempotency-Key` is **required**.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MusicGenerationRequest' }
            example: { model: vendor/music-model, prompt: Lo-fi hip hop at 80 bpm, duration_seconds: 60, format: mp3, instrumental: true }
      responses:
        '202': { $ref: '#/components/responses/JobAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '413': { $ref: '#/components/responses/RequestTooLarge' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
        '501': { $ref: '#/components/responses/NotImplemented' }
        '503': { $ref: '#/components/responses/ProviderUnavailable' }

  /v1/music/generations/{id}:
    parameters:
      - $ref: '#/components/parameters/JobId'
    get:
      tags: [Music]
      operationId: getMusicGeneration
      summary: Get a music job
      responses:
        '200': { $ref: '#/components/responses/Job' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
    delete:
      tags: [Music]
      operationId: cancelMusicGeneration
      summary: Cancel or delete a music job
      responses:
        '200': { $ref: '#/components/responses/JobCanceled' }
        '204': { description: Terminal job and its artifacts deleted. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /v1/jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/JobId'
    get:
      tags: [Jobs]
      operationId: getJob
      summary: Get any job by id
      description: Kind-agnostic view of a video or music job.
      responses:
        '200': { $ref: '#/components/responses/Job' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /health:
    get:
      tags: [Ops]
      operationId: health
      summary: Liveness
      security: []
      responses:
        '200':
          description: Process is up.
          content:
            application/json:
              schema: { type: object, properties: { status: { type: string, const: ok } } }

  /ready:
    get:
      tags: [Ops]
      operationId: ready
      summary: Readiness
      security: []
      responses:
        '200': { description: Dependencies reachable and runtime config loaded. }
        '503':
          description: Not ready.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 'sk-llm-… (40 base62 chars) or pg-… playground key'
      description: API key created in the dashboard. Only a SHA-256 hash is stored server-side.

  headers:
    X-Request-Id:
      description: Unique request id (`req_…`). Also present as `error.request_id`.
      schema: { type: string, pattern: '^req_[0-9A-Za-z]{10,32}$' }
    X-LLM-Provider:
      description: Slug of the provider that served the request (after any fallback).
      schema: { type: string }
    X-LLM-Model:
      description: Resolved gateway model id (`vendor/name`).
      schema: { type: string }
    X-LLM-Routing-Reason:
      description: Compact routing decision, e.g. `strategy=priority_cheapest selected=groq priority=10 cost_micro=42 healthy=true fallbacks=openai`.
      schema: { type: string }
    X-LLM-Cost-Micro:
      description: Charged amount in micro-USD. On streams the final usage chunk is authoritative.
      schema: { type: integer }
    Idempotent-Replayed:
      description: Present (`true`) when the response was served from the idempotency store.
      schema: { type: boolean }
    Retry-After:
      description: Seconds to wait before retrying.
      schema: { type: integer, minimum: 0 }

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: 'Optional. Same key + same body returns the stored result with `Idempotent-Replayed: true`; same key + different body → `409 idempotency_conflict`.'
      schema: { type: string, maxLength: 255 }
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      description: 'Required for async media jobs. Same key + same body returns the existing job; different body → `409 idempotency_conflict`; missing → `400 idempotency_key_required`.'
      schema: { type: string, maxLength: 255 }
    JobId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }

  responses:
    JobAccepted:
      description: Job accepted and queued.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        Idempotent-Replayed: { $ref: '#/components/headers/Idempotent-Replayed' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/JobAccepted' }
          example:
            id: 0c1f4c2a-6b1e-4f6e-9a1e-2d7d0d8b3a11
            object: video.generation
            status: queued
            model: vendor/video-model
            created_at: '2026-09-04T10:15:00Z'
            estimated_cost_micro: 400000
    Job:
      description: Job view including artifacts with signed URLs.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/MediaJob' }
    JobCanceled:
      description: Job cancelled.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/MediaJob' }
    BadRequest:
      description: Invalid request (`invalid_json`, `validation_failed`, `idempotency_key_required`, `ssrf_blocked`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { error: { type: invalid_request_error, code: validation_failed, message: 'messages must contain at least 1 item', request_id: req_01J6ZK3M9PQR7S8T9V, param: messages } }
    Unauthorized:
      description: Authentication failed (`invalid_api_key`, `key_expired`, `key_disabled`, `key_revoked`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { error: { type: authentication_error, code: invalid_api_key, message: Invalid API key, request_id: req_01J6ZK3M9PQR7S8T9V } }
    QuotaExceeded:
      description: Insufficient credits or budget (`insufficient_credits`, `budget_exceeded`, `spending_limit_exceeded`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { error: { type: quota_error, code: budget_exceeded, message: Daily budget for this API key would be exceeded, request_id: req_01J6ZK3M9PQR7S8T9V } }
    Forbidden:
      description: Not permitted (`ip_not_allowed`, `model_not_allowed`, `modality_not_allowed`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    NotFound:
      description: Not found (`model_not_found`, `not_found`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Conflict:
      description: Conflict (`idempotency_conflict`, `job_not_cancellable`). `Retry-After` present for in-flight duplicates.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        Retry-After: { $ref: '#/components/headers/Retry-After' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    RequestTooLarge:
      description: Body exceeds the size limit (`request_too_large`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    UnsupportedMediaType:
      description: Unsupported Content-Type (`unsupported_media_type`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    RateLimited:
      description: Rate limited (`rate_limited`, `tokens_per_minute_exceeded`, `concurrency_exceeded`, `provider_rate_limited`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        Retry-After: { $ref: '#/components/headers/Retry-After' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          example: { error: { type: rate_limit_error, code: rate_limited, message: Rate limit exceeded, request_id: req_01J6ZK3M9PQR7S8T9V, retry_after: 2 } }
    InternalError:
      description: Unexpected failure (`internal_error`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    NotImplemented:
      description: Feature not implemented or not configured in this deployment (`not_implemented`, `not_configured`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    ProviderError:
      description: Provider failure (`provider_error`, `provider_disconnect`, `stream_interrupted`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    ProviderUnavailable:
      description: No provider could serve the request (`no_provider_available`, `provider_unavailable`, `service_unavailable`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        Retry-After: { $ref: '#/components/headers/Retry-After' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    ProviderTimeout:
      description: Provider timed out (`provider_timeout`).
      headers: { X-Request-Id: { $ref: '#/components/headers/X-Request-Id' } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  schemas:
    ErrorType:
      type: string
      enum: [invalid_request_error, authentication_error, permission_error, rate_limit_error, quota_error, not_found_error, conflict_error, provider_error, timeout_error, internal_error]
    ErrorCode:
      type: string
      description: Stable machine-readable error code.
      enum:
        - invalid_api_key
        - key_expired
        - key_disabled
        - key_revoked
        - unauthenticated
        - invalid_credentials
        - email_not_verified
        - account_disabled
        - ip_not_allowed
        - forbidden
        - csrf_failed
        - invalid_json
        - validation_failed
        - request_too_large
        - unsupported_media_type
        - model_not_found
        - model_not_allowed
        - modality_not_allowed
        - not_found
        - conflict
        - idempotency_conflict
        - idempotency_key_required
        - rate_limited
        - tokens_per_minute_exceeded
        - concurrency_exceeded
        - insufficient_credits
        - budget_exceeded
        - spending_limit_exceeded
        - max_api_keys_reached
        - no_provider_available
        - provider_unavailable
        - provider_timeout
        - provider_rate_limited
        - provider_error
        - provider_disconnect
        - stream_interrupted
        - not_implemented
        - not_configured
        - ssrf_blocked
        - job_not_cancellable
        - service_unavailable
        - internal_error
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, code, message, request_id]
          properties:
            type: { $ref: '#/components/schemas/ErrorType' }
            code: { $ref: '#/components/schemas/ErrorCode' }
            message: { type: string }
            request_id: { type: string }
            param: { type: string, description: JSON path of the offending field. }
            provider: { type: string, description: Provider slug (never credentials). }
            retry_after: { type: integer, description: Seconds to wait before retrying. }
            details: { description: Optional structured context. }

    RoutingStrategy:
      type: string
      enum: [priority_cheapest, cheapest, fastest, priority, highest_availability, smart]
      description: '`smart` is not yet available and falls back to `priority_cheapest`.'
    RoutingOverride:
      type: object
      description: Platform extension. Stripped before forwarding to the provider.
      properties:
        strategy: { $ref: '#/components/schemas/RoutingStrategy' }
        providers:
          type: array
          maxItems: 10
          items: { type: string }
          description: Restrict candidates to these provider slugs (preference order).

    PriceSheet:
      type: object
      description: Customer list price in integer micro-USD. Only keys relevant to the modality are present.
      properties:
        input_per_mtok: { type: integer }
        output_per_mtok: { type: integer }
        cached_input_per_mtok: { type: integer }
        per_request: { type: integer }
        per_image: { type: integer }
        per_audio_second: { type: integer }
        per_video_second: { type: integer }
        per_music_second: { type: integer }
        per_character: { type: integer }
        currency: { type: string, const: USD }
        unit: { type: string, const: micro }
    Model:
      type: object
      required: [id, object, created, owned_by, display_name, modality, input_modalities, output_modalities, pricing, discount_bps, capabilities, supported_parameters, providers]
      properties:
        id: { type: string, description: '`vendor/name`' }
        object: { type: string, const: model }
        created: { type: integer }
        owned_by: { type: string }
        display_name: { type: string }
        modality: { type: string, enum: [text, image, audio, video, music, embedding] }
        input_modalities: { type: array, items: { type: string } }
        output_modalities: { type: array, items: { type: string } }
        context_window: { type: [integer, 'null'] }
        max_output_tokens: { type: [integer, 'null'] }
        pricing: { $ref: '#/components/schemas/PriceSheet' }
        discount_bps: { type: integer, description: Discount in basis points (100 = 1%). }
        capabilities: { type: object, additionalProperties: { type: boolean } }
        supported_parameters: { type: array, items: { type: string } }
        providers: { type: integer, description: Number of enabled providers serving this model. }
    ModelList:
      type: object
      required: [object, data]
      properties:
        object: { type: string, const: list }
        data: { type: array, items: { $ref: '#/components/schemas/Model' } }

    ContentPart:
      oneOf:
        - type: object
          required: [type, text]
          properties: { type: { const: text }, text: { type: string } }
        - type: object
          required: [type, image_url]
          properties:
            type: { const: image_url }
            image_url:
              type: object
              required: [url]
              properties: { url: { type: string }, detail: { type: string, enum: [auto, low, high] } }
        - type: object
          required: [type, input_audio]
          properties:
            type: { const: input_audio }
            input_audio:
              type: object
              required: [data, format]
              properties: { data: { type: string }, format: { type: string } }
    ToolCall:
      type: object
      required: [id, type, function]
      properties:
        id: { type: string }
        type: { type: string, const: function }
        function:
          type: object
          required: [name, arguments]
          properties: { name: { type: string }, arguments: { type: string } }
    ChatMessage:
      type: object
      required: [role]
      properties:
        role: { type: string, enum: [system, user, assistant, tool, developer] }
        content:
          oneOf:
            - type: string
            - type: array
              items: { $ref: '#/components/schemas/ContentPart' }
            - type: 'null'
        name: { type: string }
        tool_call_id: { type: string }
        tool_calls: { type: array, items: { $ref: '#/components/schemas/ToolCall' } }
    Tool:
      type: object
      required: [type, function]
      properties:
        type: { type: string, const: function }
        function:
          type: object
          required: [name]
          properties:
            name: { type: string }
            description: { type: string }
            parameters: { type: object, additionalProperties: true }
            strict: { type: boolean }
    ChatCompletionRequest:
      type: object
      required: [model, messages]
      properties:
        model: { type: string, minLength: 1 }
        messages: { type: array, minItems: 1, items: { $ref: '#/components/schemas/ChatMessage' } }
        stream: { type: boolean }
        stream_options:
          type: object
          properties: { include_usage: { type: boolean } }
          description: Accepted for compatibility; a final usage chunk is always emitted.
        temperature: { type: number, minimum: 0, maximum: 2 }
        top_p: { type: number, minimum: 0, maximum: 1 }
        max_tokens: { type: integer, minimum: 1 }
        max_completion_tokens: { type: integer, minimum: 1 }
        stop:
          oneOf:
            - type: string
            - type: array
              maxItems: 4
              items: { type: string }
        n: { type: integer, const: 1, description: Only 1 is supported. }
        presence_penalty: { type: number, minimum: -2, maximum: 2 }
        frequency_penalty: { type: number, minimum: -2, maximum: 2 }
        logit_bias: { type: object, additionalProperties: { type: number } }
        seed: { type: integer }
        user: { type: string, maxLength: 256 }
        tools: { type: array, items: { $ref: '#/components/schemas/Tool' } }
        tool_choice:
          oneOf:
            - type: string
              enum: [none, auto, required]
            - type: object
              required: [type, function]
              properties:
                type: { type: string, const: function }
                function: { type: object, required: [name], properties: { name: { type: string } } }
        parallel_tool_calls: { type: boolean }
        response_format:
          oneOf:
            - type: object
              required: [type]
              properties: { type: { const: text } }
            - type: object
              required: [type]
              properties: { type: { const: json_object } }
            - type: object
              required: [type, json_schema]
              properties: { type: { const: json_schema }, json_schema: { type: object, additionalProperties: true } }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    Usage:
      type: object
      required: [prompt_tokens, completion_tokens, total_tokens]
      properties:
        prompt_tokens: { type: integer }
        completion_tokens: { type: integer }
        total_tokens: { type: integer }
        prompt_tokens_details:
          type: object
          properties: { cached_tokens: { type: integer } }
        x_llm_usage_estimated: { type: boolean, description: Platform extension — token counts were estimated. }
        x_llm_cost_micro: { type: integer, description: Platform extension — charged amount in micro-USD. }
    ChatCompletionResponse:
      type: object
      required: [id, object, created, model, choices, usage]
      properties:
        id: { type: string }
        object: { type: string, const: chat.completion }
        created: { type: integer }
        model: { type: string }
        provider: { type: string, description: Platform extension — provider that served the request. }
        choices:
          type: array
          items:
            type: object
            required: [index, message, finish_reason]
            properties:
              index: { type: integer }
              message: { $ref: '#/components/schemas/ChatMessage' }
              finish_reason: { type: [string, 'null'] }
        usage: { $ref: '#/components/schemas/Usage' }
    ChatCompletionChunk:
      type: object
      required: [id, object, created, model, choices]
      properties:
        id: { type: string }
        object: { type: string, const: chat.completion.chunk }
        created: { type: integer }
        model: { type: string }
        provider: { type: string }
        choices:
          type: array
          items:
            type: object
            required: [index, delta, finish_reason]
            properties:
              index: { type: integer }
              delta:
                type: object
                properties:
                  role: { type: string }
                  content: { type: [string, 'null'] }
                  tool_calls: { type: array, items: { type: object, additionalProperties: true } }
              finish_reason: { type: [string, 'null'] }
        usage:
          oneOf:
            - $ref: '#/components/schemas/Usage'
            - type: 'null'

    CompletionRequest:
      type: object
      required: [model, prompt]
      properties:
        model: { type: string }
        prompt:
          oneOf:
            - type: string
            - type: array
              items: { type: string }
        max_tokens: { type: integer, minimum: 1 }
        temperature: { type: number, minimum: 0, maximum: 2 }
        top_p: { type: number, minimum: 0, maximum: 1 }
        stop:
          oneOf:
            - type: string
            - type: array
              maxItems: 4
              items: { type: string }
        stream: { type: boolean }
        n: { type: integer, const: 1 }
        presence_penalty: { type: number }
        frequency_penalty: { type: number }
        seed: { type: integer }
        user: { type: string }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    CompletionResponse:
      type: object
      required: [id, object, created, model, choices, usage]
      properties:
        id: { type: string }
        object: { type: string, const: text_completion }
        created: { type: integer }
        model: { type: string }
        provider: { type: string }
        choices:
          type: array
          items:
            type: object
            properties:
              index: { type: integer }
              text: { type: string }
              finish_reason: { type: [string, 'null'] }
        usage: { $ref: '#/components/schemas/Usage' }

    ResponsesRequest:
      type: object
      required: [model, input]
      properties:
        model: { type: string }
        input:
          oneOf:
            - type: string
            - type: array
              items: { $ref: '#/components/schemas/ChatMessage' }
        instructions: { type: string }
        max_output_tokens: { type: integer, minimum: 1 }
        temperature: { type: number, minimum: 0, maximum: 2 }
        stream: { type: boolean }
        tools: { type: array, items: { type: object, additionalProperties: true } }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    ResponsesResponse:
      type: object
      required: [id, object, model, output, usage]
      properties:
        id: { type: string }
        object: { type: string, const: response }
        created_at: { type: integer }
        model: { type: string }
        provider: { type: string }
        status: { type: string, enum: [completed, incomplete, failed] }
        output:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              id: { type: string }
              type: { type: string, enum: [message, function_call] }
              role: { type: string }
              status: { type: string }
              content:
                type: array
                items:
                  type: object
                  properties: { type: { type: string }, text: { type: string } }
        usage:
          type: object
          properties:
            input_tokens: { type: integer }
            output_tokens: { type: integer }
            total_tokens: { type: integer }
            x_llm_cost_micro: { type: integer }
            x_llm_usage_estimated: { type: boolean }

    EmbeddingRequest:
      type: object
      required: [model, input]
      properties:
        model: { type: string }
        input:
          oneOf:
            - type: string
            - type: array
              maxItems: 2048
              items: { type: string }
        encoding_format: { type: string, enum: [float, base64] }
        dimensions: { type: integer, minimum: 1 }
        user: { type: string }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    EmbeddingResponse:
      type: object
      required: [object, data, model, usage]
      properties:
        object: { type: string, const: list }
        model: { type: string }
        provider: { type: string }
        data:
          type: array
          items:
            type: object
            required: [object, index, embedding]
            properties:
              object: { type: string, const: embedding }
              index: { type: integer }
              embedding:
                oneOf:
                  - type: array
                    items: { type: number }
                  - type: string
                    description: base64 when `encoding_format` is `base64`.
        usage:
          type: object
          properties:
            prompt_tokens: { type: integer }
            total_tokens: { type: integer }
            x_llm_cost_micro: { type: integer }

    ImageGenerationRequest:
      type: object
      required: [model, prompt]
      properties:
        model: { type: string }
        prompt: { type: string, minLength: 1, maxLength: 32000 }
        n: { type: integer, minimum: 1, maximum: 10, default: 1 }
        size: { type: string, pattern: '^\d{2,5}x\d{2,5}$', examples: [1024x1024] }
        quality: { type: string, enum: [standard, hd, low, medium, high, auto] }
        style: { type: string }
        response_format: { type: string, enum: [url, b64_json], default: url }
        input_images:
          type: array
          maxItems: 10
          items: { type: string }
          description: Reference images as https URLs (SSRF-guarded) or base64.
        user: { type: string }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    ImageEditMultipart:
      type: object
      required: [image, prompt, model]
      properties:
        image: { type: string, format: binary }
        mask: { type: string, format: binary }
        prompt: { type: string }
        model: { type: string }
        n: { type: integer, minimum: 1, maximum: 10 }
        size: { type: string }
    ImageResponse:
      type: object
      required: [created, data]
      properties:
        created: { type: integer }
        provider: { type: string }
        data:
          type: array
          items:
            type: object
            properties:
              url: { type: string, description: 'Signed URL (about 1 h) when storage is configured, else provider URL.' }
              b64_json: { type: string }
              revised_prompt: { type: string }
        usage:
          type: object
          properties:
            images: { type: integer }
            x_llm_cost_micro: { type: integer }

    SpeechRequest:
      type: object
      required: [model, input, voice]
      properties:
        model: { type: string }
        input: { type: string, minLength: 1, maxLength: 40000 }
        voice: { type: string }
        response_format: { type: string, enum: [mp3, opus, aac, flac, wav, pcm], default: mp3 }
        speed: { type: number, minimum: 0.25, maximum: 4 }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    TranscriptionMultipart:
      type: object
      required: [file, model]
      properties:
        file: { type: string, format: binary }
        model: { type: string }
        language: { type: string, description: ISO-639-1 code. }
        response_format: { type: string, enum: [json, text, verbose_json, srt, vtt] }
    TranscriptionResponse:
      type: object
      required: [text]
      additionalProperties: true
      properties:
        text: { type: string }
        language: { type: string }
        duration: { type: number }
        segments: { type: array, items: { type: object, additionalProperties: true } }

    VideoGenerationRequest:
      type: object
      required: [model, prompt]
      properties:
        model: { type: string }
        prompt: { type: string, minLength: 1, maxLength: 10000 }
        duration_seconds: { type: number, minimum: 1, maximum: 120 }
        image_url: { type: string, format: uri }
        end_image_url: { type: string, format: uri }
        input_images: { type: array, maxItems: 10, items: { type: string } }
        resolution: { type: string, examples: [1080p] }
        aspect_ratio: { type: string, pattern: '^\d+:\d+$', examples: ['16:9'] }
        audio: { type: boolean }
        webhook_url: { type: string, format: uri, description: 'https, public host only (SSRF-guarded). Receives `{event, job}` signed with `X-LLM-Signature`.' }
        metadata: { type: object, additionalProperties: { type: string } }
        max_cost_micro: { type: integer, minimum: 1, description: Cap on the cost reservation (default 1.5 × estimate). }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    MusicGenerationRequest:
      type: object
      required: [model, prompt]
      properties:
        model: { type: string }
        prompt: { type: string, minLength: 1, maxLength: 10000 }
        duration_seconds: { type: number, minimum: 1, maximum: 600 }
        format: { type: string, enum: [mp3, wav, flac] }
        sample_rate: { type: integer }
        instrumental: { type: boolean }
        webhook_url: { type: string, format: uri }
        metadata: { type: object, additionalProperties: { type: string } }
        max_cost_micro: { type: integer, minimum: 1 }
        routing: { $ref: '#/components/schemas/RoutingOverride' }
    JobStatus:
      type: string
      enum: [queued, submitted, running, succeeded, failed, canceled, expired]
      description: Terminal states are succeeded, failed, canceled, expired.
    JobAccepted:
      type: object
      required: [id, object, status, model, created_at, estimated_cost_micro]
      properties:
        id: { type: string, format: uuid }
        object: { type: string, enum: [video.generation, music.generation] }
        status: { $ref: '#/components/schemas/JobStatus' }
        model: { type: string }
        created_at: { type: string, format: date-time }
        estimated_cost_micro: { type: integer }
    Artifact:
      type: object
      required: [id, type, content_type, url]
      properties:
        id: { type: string, format: uuid }
        type: { type: string, examples: [video, audio] }
        content_type: { type: string }
        size_bytes: { type: [integer, 'null'] }
        width: { type: [integer, 'null'] }
        height: { type: [integer, 'null'] }
        duration_seconds: { type: [number, 'null'] }
        url: { type: [string, 'null'], description: Short-lived signed URL (or provider URL when storage is disabled). }
        expires_at: { type: [string, 'null'], format: date-time }
    MediaJob:
      type: object
      required: [id, object, request_id, kind, model, status, estimated_cost_micro, artifacts, created_at]
      properties:
        id: { type: string, format: uuid }
        object: { type: string, enum: [video.generation, music.generation, job] }
        request_id: { type: string }
        org_id: { type: string, format: uuid }
        kind: { type: string, enum: [video, music, audio, image] }
        model: { type: string }
        provider: { type: [string, 'null'] }
        status: { $ref: '#/components/schemas/JobStatus' }
        progress: { type: [integer, 'null'], minimum: 0, maximum: 100 }
        estimated_cost_micro: { type: integer }
        max_cost_micro: { type: [integer, 'null'] }
        cost_micro: { type: [integer, 'null'] }
        error:
          oneOf:
            - type: object
              required: [code, message]
              properties: { code: { type: string }, message: { type: string } }
            - type: 'null'
        params: { type: object, additionalProperties: true }
        artifacts: { type: array, items: { $ref: '#/components/schemas/Artifact' } }
        webhook_url: { type: [string, 'null'] }
        created_at: { type: string, format: date-time }
        started_at: { type: [string, 'null'], format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        expires_at: { type: [string, 'null'], format: date-time }
    WebhookPayload:
      type: object
      description: 'Body POSTed to `webhook_url`. Header `X-LLM-Signature: sha256=<hex HMAC-SHA256(body, org webhook secret)>`.'
      required: [event, job]
      properties:
        event: { type: string, enum: [job.completed, job.failed] }
        job: { $ref: '#/components/schemas/MediaJob' }

webhooks:
  jobFinished:
    post:
      operationId: jobFinishedWebhook
      summary: Job completed or failed
      description: Sent to the job's `webhook_url` when it reaches `succeeded` or `failed`. Verify `X-LLM-Signature` before trusting the payload.
      parameters:
        - name: X-LLM-Signature
          in: header
          required: true
          schema: { type: string, pattern: '^sha256=[0-9a-f]{64}$' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookPayload' }
      responses:
        '2xx': { description: Acknowledged. }
