# ─────────────────────────────────────────────────────────────────────
# Stav AI — Public API spec
#
# Hand-curated OpenAPI 3.1 spec covering ONLY the customer-facing surface
# (inference + public catalogue browse). The internal portal-API
# Swagger is deliberately disabled (docs_url=None) and nginx 404s the
# old `/docs` and `/openapi.json` paths.
#
# Strategy & implementation docs live in the internal docs/ tree
# (developer docs strategy + implementation plan).
#
# This file is the contract. Endpoints listed here are long-term
# customer commitments. Endpoints in the implementation but NOT in this
# spec are internal and may change without notice.
#
# PARTLY GENERATED. Two regions are derived from
# `heimdall-inference/app/params/registry.py` — the single declaration of
# the request-parameter contract — and are marked in place:
#
#   * the parameter-support table in `info.description`
#   * `components.schemas.ChatCompletionRequest.properties`
#
# Do not hand-edit inside the markers. After changing the registry, run:
#
#     python3 scripts/gen-openapi-params.py --write
#
# Without --write it prints a diff and exits 1, so CI can gate on drift.
# Everything outside the markers is hand-curated.
# ─────────────────────────────────────────────────────────────────────
openapi: 3.1.0

info:
  title: Stav AI API
  version: "1.0.0"
  summary: EU-sovereign AI inference platform
  description: |
    The Stav AI API gives you OpenAI-compatible and Anthropic-compatible
    inference against a curated catalogue of commercial models, with routing,
    a per-team sovereignty floor, and a per-request audit trail.

    ## Two SDK contracts, one API

    - **OpenAI-compatible** (`/v1/chat/completions`, `/v1/embeddings`,
      `/v1/models`) — drop-in for the official OpenAI SDKs. Set
      `base_url="https://api.stav.ai/v1"` and your existing code works.
    - **Anthropic-compatible** (`/v1/messages`) — drop-in for the official
      Anthropic SDK. Set `base_url="https://api.stav.ai"` (the SDK appends
      `/v1` itself) and
      `claude-*` models proxy verbatim to Anthropic (preserving
      `cache_control`, `thinking`, beta features). Every other catalogue
      model translates to the OpenAI Chat Completions contract under the
      hood — same call, same shape, different upstream.

    ## Sovereignty model

    Every inference response carries an `X-Stav-Sovereignty-Level` header
    with the effective sovereignty level of the endpoint that served the
    request, on Stav's graded L0–L4 ladder:

    - **`L0`** — foreign jurisdiction (served under third-country law,
      outside the EU/EEA guarantees).
    - **`L1`** — EU/EEA location: inference runs on EU/EEA soil.
    - **`L2`** — EU/EEA operation + certification.
    - **`L3`** — EU/EEA ownership & control of the serving operator.
    - **`L4`** — full sovereignty, no exceptions.
    - **`UNCLASSIFIED`** — not yet assessed (a data-quality state, not a
      rung).

    The same level is recorded on every request log entry, so your
    compliance evidence can state per request which rung served it.

    Models are proxied through Stav from their upstream providers (OpenAI,
    Anthropic, Mistral, Google, xAI, Cohere and others). Stav holds the
    upstream relationship, so your traffic terms are governed by Stav's Terms
    of Service rather than the commercial vendor's, and every call is logged,
    billed and audit-trailed by Stav.

    A routing floor is set per team (and optionally raised per router);
    it applies to routing decisions, and combines by taking the strictest
    value. See [the sovereignty ladder](https://stav.ai/docs/concepts/sovereignty).

    ## Smart Router

    Pass `model="auto"` to let Stav's Smart Router pick a model for the
    request, or `@team-slug/router-slug` to use a named routing policy your
    team authored. Selection scores the eligible pool on quality, expected
    request cost and measured end-to-end latency using your team's weights,
    after hard-filtering on your sovereignty floor, quality floor and the
    capabilities the request needs.

    What the response tells you:

    - `model` — the model that actually served, which may differ from what
      you sent.
    - `X-Stav-Model`, `X-Stav-Provider`, `X-Stav-Model-Type` — the same, in
      headers.
    - `X-Stav-Route-Reason` — a one-line, human-readable rationale. Prose;
      do not parse it.
    - `X-Stav-Router` — the router that handled the request, when one did.

    The full decision — every candidate, its score, and the alternatives
    retained for failover — is in the Customer Portal under
    **Monitor → Routing**.

    ## Request parameters

    Stav does not silently drop parameters a provider cannot honour. A
    parameter is either honoured, or refused with a `400`
    (`unsupported_parameter`) naming the parameter and the model that
    resolved. An undeclared field is likewise refused by name.

    Support is keyed by wire dialect, not vendor: the `openai` dialect also
    backs xAI, Cohere, Azure and OpenAI-compatible servers.

    <!-- GENERATED: parameter support table — do not edit by hand -->
    | Parameter | OpenAI | Anthropic | Google | Mistral |
    | --- | --- | --- | --- | --- |
    | `temperature` | yes | yes | yes | yes |
    | `top_p` | yes | yes | yes | yes |
    | `max_tokens` | yes | yes | yes | yes |
    | `response_format` | yes | yes | yes | yes |
    | `tools` | yes | yes | yes | yes |
    | `tool_choice` | yes | yes | yes | yes |
    | `reasoning_effort` *(`reasoning`, `thinking`)* | yes | yes | yes | yes |
    | `stop` | yes | yes | yes | yes |
    | `top_k` | **400** | yes | yes | **400** |
    | `seed` | yes | **400** | **400** | **400** |
    | `logprobs` | yes | **400** | **400** | **400** |
    | `top_logprobs` | yes | **400** | **400** | **400** |
    | `presence_penalty` | yes | **400** | **400** | **400** |
    | `frequency_penalty` | yes | **400** | **400** | **400** |
    | `logit_bias` | yes | **400** | **400** | **400** |
    | `verbosity` | yes | **400** | **400** | **400** |
    | `prediction` | yes | **400** | **400** | **400** |
    | `prompt_cache_key` | yes | **400** | **400** | **400** |
    | `safety_identifier` | yes | no-op | no-op | no-op |
    | `service_tier` | yes | **400** | **400** | **400** |
    | `user` | yes | no-op | no-op | no-op |
    | `parallel_tool_calls` | yes | **400** | **400** | **400** |
    | `min_p` | **400** | **400** | **400** | **400** |
    | `repetition_penalty` | **400** | **400** | **400** | **400** |
    | `store` | **400** | **400** | **400** | **400** |
    | `metadata` | **400** | **400** | **400** | **400** |
    | `modalities` | **400** | **400** | **400** | **400** |
    | `include_reasoning` | **400** | **400** | **400** | **400** |
    | `chat_template_kwargs` | **400** | **400** | **400** | **400** |
    <!-- END GENERATED -->

    Per-model capabilities are discoverable before you send anything —
    `GET /v1/models/{model_id}` returns `stav_capabilities`.

    ## Authentication

    All authenticated endpoints take a Bearer token in the `Authorization`
    header:

    ```
    Authorization: Bearer sk_stav_live_...
    ```

    Keys are `sk_stav_live_...` (production) or `sk_stav_test_...` (sandbox);
    user-scoped keys carry a `sk_stav_user_` prefix. Legacy `hm_*` keys are
    no longer accepted. `x-api-key` is accepted as an alternative header, for
    Anthropic-SDK compatibility; `Authorization` wins when both are present.

    A key may be restricted to a subset of scopes — `inference:chat`,
    `inference:messages`, `inference:embeddings`, `inference:models`.
    **A key with no scopes set has full access**; scopes
    are opt-in restriction. Note that the OpenAI SDK calls `GET /v1/models`,
    which needs `inference:models`.

    `GET /v1/auth/validate` echoes back the team, key name, environment and
    scopes attached to a key — the cheapest way to check a deployment.

    To create a key, go to the [Customer Portal → Connect → Keys](https://stav.ai/app/connect/keys).
    Keep the secret value safe — it's shown once at creation and never again.

    ## Rate limits

    Default 60 requests/minute per key; your plan tier, the key's
    environment and any per-key override determine the effective number.
    Headers on a successful response report:

    - `X-RateLimit-Limit` — your plan's per-minute cap
    - `X-RateLimit-Remaining` — requests left in this window
    - `X-RateLimit-Reset` — Unix timestamp when the window rolls over

    Exceeding the cap returns `429` with a `Retry-After` header.

    ## Request IDs

    Every response carries `X-Stav-Request-Id` — on a success it is the same
    value as the completion `id`. Quote it when contacting support.

    Every **error** carries the same identifier twice: on the
    `X-Stav-Request-Id` header, and as `error.request_id` in the body. That
    holds for streaming errors too, where the SSE error frame carries
    `error.request_id` even though the HTTP status is `200`.
  termsOfService: https://stav.ai/legal/terms
  contact:
    name: Stav AI Support
    url: https://stav.ai/support
    email: support@stav.ai
  license:
    name: Proprietary
    url: https://stav.ai/legal/terms

servers:
  - url: https://api.stav.ai
    description: Production
  - url: https://staging-api.stav.ai
    description: Staging (rate-limited, may reset state without notice)

tags:
  - name: Chat
    description: |
      Conversational and tool-using inference. Two contracts available:
      OpenAI-compatible (`/v1/chat/completions`) and Anthropic-compatible
      (`/v1/messages`). Pick the one whose SDK you already use.
  - name: Embeddings
    description: Vector embeddings for search, clustering, and retrieval.
  - name: Models
    description: |
      Models accessible to the authenticated caller. Filtered by key
      scope and team plan.
  - name: Catalogue
    description: |
      Public, anonymous browse of the full Stav model catalogue. No
      auth required. ETag-cached, rate-limited 60 req/min/IP.

security:
  - bearerAuth: []

paths:
  # ───────────────────────────────────────────────────────────────────
  # Inference — Chat
  # ───────────────────────────────────────────────────────────────────
  /v1/chat/completions:
    post:
      tags: [Chat]
      summary: Create a chat completion (OpenAI-compatible)
      operationId: createChatCompletion
      description: |
        Generates a model response for a conversation. Drop-in for the
        OpenAI Python or TypeScript SDK — set `base_url="https://api.stav.ai/v1"`
        and your existing code works.

        Supports streaming via SSE (`stream=true`), function calling
        (`tools[]`), structured output (`response_format`), extended
        thinking (`reasoning_effort`), and prompt caching markers
        (`cache_control` on messages, tools and the system prompt, where
        the underlying model supports it).

        Pass `model="auto"` to invoke the Smart Router, or
        `@team-slug/router-slug` to use one of your team's named routers.

        `tools[]` are your own functions: Stav returns the call the model
        wants to make and your application executes it. Stav does not run
        tools server-side.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatCompletionRequest'
            examples:
              simple:
                summary: Simple completion
                value:
                  model: qwen3-32b
                  messages:
                    - role: user
                      content: "Explain EU AI Act Article 26 in three bullets."
              with_reasoning:
                summary: Extended thinking
                value:
                  model: auto
                  messages:
                    - role: user
                      content: "Prove that the square root of two is irrational."
                  reasoning_effort: high
              smart_router:
                summary: Smart Router (model=auto)
                value:
                  model: auto
                  messages:
                    - role: user
                      content: "Write a Python script to deduplicate this CSV."
              streaming:
                summary: Streaming response
                value:
                  model: qwen3-32b
                  messages:
                    - role: user
                      content: "Tell me a story."
                  stream: true
      responses:
        '200':
          description: Successful completion.
          headers:
            X-Stav-Request-Id:
              $ref: '#/components/headers/RequestId'
            X-Stav-Model:
              $ref: '#/components/headers/StavModel'
            X-Stav-Provider:
              $ref: '#/components/headers/StavProvider'
            X-Stav-Model-Type:
              $ref: '#/components/headers/StavModelType'
            X-Stav-Sovereignty-Level:
              $ref: '#/components/headers/SovereigntyLevel'
            X-Stav-Route-Reason:
              $ref: '#/components/headers/StavRouteReason'
            X-Stav-Router:
              $ref: '#/components/headers/StavRouter'
            X-Stav-Route-Time-Ms:
              $ref: '#/components/headers/StavRouteTimeMs'
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatCompletionResponse'
            text/event-stream:
              schema:
                type: string
                description: |
                  Server-sent events stream. Each event is a `data:` line
                  containing a JSON-encoded `ChatCompletionChunk`,
                  terminated by `data: [DONE]`.
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }

  # ───────────────────────────────────────────────────────────────────
  # Inference — Messages (Anthropic-compatible)
  # ───────────────────────────────────────────────────────────────────
  /v1/messages:
    post:
      tags: [Chat]
      summary: Create a message (Anthropic-compatible)
      operationId: createMessage
      description: |
        Anthropic Messages API. Drop-in for the official Anthropic SDK —
        set `base_url="https://api.stav.ai"` (the SDK appends `/v1`) and
        `api_key=sk_stav_live_...`.

        **Routing:**

        - `claude-*` models are proxied verbatim to `api.anthropic.com`.
          Full Messages-API fidelity including `cache_control` markers,
          extended `thinking`, and beta features.
        - Every other catalogue model is translated to the OpenAI Chat Completions
          contract and dispatched through the same path as
          `/v1/chat/completions`. `model="auto"` invokes the Smart Router.

        Per-key rate limits apply. App attribution via the `X-Stav-App-Id`
        header is supported and recorded on the inference log.
      parameters:
        - in: header
          name: X-Stav-App-Id
          required: false
          schema: { type: string }
          description: Optional app slug for attribution on the inference log.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessagesRequest'
            examples:
              claude:
                summary: Claude (proxied to Anthropic)
                value:
                  model: claude-sonnet-4-5
                  max_tokens: 1024
                  messages:
                    - role: user
                      content: "Summarise CLOUD Act Section 102."
              routed:
                summary: Smart Router via the Anthropic contract
                value:
                  model: auto
                  max_tokens: 1024
                  messages:
                    - role: user
                      content: "Same prompt, routed."
      responses:
        '200':
          description: |
            When `stream=false` (default), returns an Anthropic `Message`
            JSON object. When `stream=true`, returns a `text/event-stream`
            Anthropic SSE response with events `message_start`,
            `content_block_start`, `content_block_delta`,
            `content_block_stop`, `message_delta`, `message_stop`,
            `ping`, `error`.
          headers:
            X-Stav-Request-Id:
              $ref: '#/components/headers/RequestId'
            X-Stav-Model:
              $ref: '#/components/headers/StavModel'
            X-Stav-Provider:
              $ref: '#/components/headers/StavProvider'
            X-Stav-Model-Type:
              $ref: '#/components/headers/StavModelType'
            X-Stav-Sovereignty-Level:
              $ref: '#/components/headers/SovereigntyLevel'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessagesResponse'
            text/event-stream:
              schema:
                type: string
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
        '503':
          description: Upstream Anthropic credential not configured on this deployment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/responses:
    post:
      tags: [Chat]
      summary: Create a response (OpenAI Responses API, passthrough)
      operationId: createResponse
      description: |
        OpenAI Responses API. Faithful passthrough to OpenAI for
        routed-commercial **OpenAI** models — drop-in for the official
        OpenAI SDK: set `base_url="https://api.stav.ai/v1"` and
        `api_key=sk_stav_live_...`, then call `client.responses.create(...)`.

        This is the only surface for OpenAI's **Responses-API-only** models
        (`gpt-5-codex`, `codex-*`, `o*-pro`), which cannot be served on
        `/v1/chat/completions`. Dual-surface models (`gpt-5`, `o`-series)
        are also accepted here so their reasoning features (`reasoning`,
        encrypted reasoning, built-in tools) are fully expressed.

        For an OpenAI model the request body is forwarded verbatim (with
        the Stav model slug mapped to the provider model id) and the response /
        SSE stream is returned untouched.

        **Every other catalogue model is also accepted**, and is translated onto
        Stav's chat dispatch path — so sovereign and open-weight models work on
        this surface too. Two differences apply on the translated path:

        * Server-side conversation state is not supported. `store: true` and
          `previous_response_id` are refused by name; send the full input each
          turn. (Stav keeps no hidden conversation state — every turn is
          visible in the audit log.)
        * Hosted tools the target provider cannot execute — `web_search` and
          friends — are dropped and named in the `X-Stav-Dropped-Params`
          response header. This is a capability limit of that provider's chat
          API, not a policy decision: a model's own native tools are accepted
          along with the model wherever the provider can run them.

        ### Prompt retention — `store` defaults to `false` on Stav

        OpenAI's own Responses API defaults `store` to `true`, retaining your
        prompt and completion on OpenAI's infrastructure. **Stav inverts that
        default.** A request that does not mention `store` is forwarded with
        `store: false`, so nothing is retained upstream.

        The consequence is deliberate and worth stating plainly:
        `GET /v1/responses/{response_id}` and
        `GET /v1/responses/{response_id}/input_items` **require opting in**
        with `store: true`. Retrieval is a real feature and it is yours to
        turn on — it is simply not on by default, because the default should
        not quietly park customer content in a jurisdiction you did not
        choose.

        Setting `store` explicitly — either value — is forwarded unchanged.
        On the translated (non-OpenAI) path `store: true` is refused by name,
        because Stav keeps no server-side conversation state of its own.

        ### What this surface does and does not do

        For an explicitly named **OpenAI** model this is a verbatim proxy:
        there is no endpoint-level failover, because there is one upstream.
        `model: auto` and `model: "@team/router"` **are** honoured here and
        route exactly as they do on `/v1/chat/completions`; your team's
        sovereignty floor is enforced on both paths.

        Per-key rate limits apply; one inference-log row is written per call.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: >
                OpenAI Responses API request body. See the OpenAI Responses
                API reference. `model` is any catalogue slug: OpenAI models are
                forwarded verbatim, everything else is translated.
              required: [model]
              properties:
                model: { type: string, example: gpt-5-codex }
                input:
                  description: String or array of input items (OpenAI Responses shape).
                  oneOf:
                    - { type: string }
                    - { type: array, items: { type: object, additionalProperties: true } }
                stream: { type: boolean, default: false }
                store:
                  type: boolean
                  default: false
                  description: >
                    Retain the prompt and completion upstream so the response
                    can be fetched later by id. Defaults to `false` on Stav —
                    OpenAI's own default is `true`. Required for
                    `GET /v1/responses/{response_id}` and its `input_items`.
              additionalProperties: true
            examples:
              codex:
                summary: Codex (Responses-only)
                value:
                  model: gpt-5-codex
                  input: "Write a Python function that reverses a linked list."
      responses:
        '200':
          description: |
            When `stream=false` (default), returns an OpenAI Responses
            object. When `stream=true`, returns a `text/event-stream` of
            the OpenAI Responses event protocol (`response.created`,
            `response.output_text.delta`, …, `response.completed`).
          headers:
            X-Stav-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
            text/event-stream:
              schema: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
        '503':
          description: Upstream OpenAI credential not configured on this deployment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /v1/responses/{response_id}:
    parameters:
      - in: path
        name: response_id
        required: true
        schema: { type: string }
        description: Id of a stored response (from a prior POST /v1/responses).
    get:
      tags: [Chat]
      summary: Retrieve a stored response
      operationId: getResponse
      description: |
        OpenAI Responses passthrough — retrieve a stored response by id.
        Requires the original request to have set `store: true` explicitly:
        Stav defaults `store` to `false`, unlike OpenAI. Without that opt-in
        there is nothing stored to retrieve and this returns `404`. Query
        params (e.g. `include`) are forwarded.
      responses:
        '200':
          description: The stored OpenAI Responses object.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '502': { $ref: '#/components/responses/UpstreamError' }
    delete:
      tags: [Chat]
      summary: Delete a stored response
      operationId: deleteResponse
      description: OpenAI Responses passthrough — delete a stored response by id.
      responses:
        '200':
          description: Deletion confirmation object.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '502': { $ref: '#/components/responses/UpstreamError' }

  /v1/responses/{response_id}/cancel:
    post:
      tags: [Chat]
      summary: Cancel an in-progress (background) response
      operationId: cancelResponse
      parameters:
        - in: path
          name: response_id
          required: true
          schema: { type: string }
      description: |
        OpenAI Responses passthrough — cancel a response created with
        `background: true` that is still in progress.
      responses:
        '200':
          description: The updated (cancelling/cancelled) response object.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '502': { $ref: '#/components/responses/UpstreamError' }

  /v1/responses/{response_id}/input_items:
    get:
      tags: [Chat]
      summary: List input items for a stored response
      operationId: listResponseInputItems
      parameters:
        - in: path
          name: response_id
          required: true
          schema: { type: string }
        - in: query
          name: limit
          required: false
          schema: { type: integer }
        - in: query
          name: order
          required: false
          schema: { type: string, enum: [asc, desc] }
        - in: query
          name: after
          required: false
          schema: { type: string }
        - in: query
          name: before
          required: false
          schema: { type: string }
      description: |
        OpenAI Responses passthrough — list the input items for a stored
        response. Requires the original request to have set `store: true`
        explicitly — Stav defaults `store` to `false`, unlike OpenAI — so
        without that opt-in this returns `404`. Pagination query params are
        forwarded verbatim.
      responses:
        '200':
          description: A paginated list of input items.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '502': { $ref: '#/components/responses/UpstreamError' }

  # ───────────────────────────────────────────────────────────────────
  # Inference — Embeddings
  # ───────────────────────────────────────────────────────────────────
  /v1/embeddings:
    post:
      tags: [Embeddings]
      summary: Create embeddings
      operationId: createEmbedding
      description: |
        Generates vector embeddings for one or more inputs. Drop-in for
        the OpenAI SDK.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmbeddingRequest'
            examples:
              single:
                value:
                  model: gemini-embedding-2
                  input: "Sovereignty is not a feature, it is a contract."
              batch:
                value:
                  model: bge-m3
                  input:
                    - "First chunk of text."
                    - "Second chunk of text."
                    - "Third chunk of text."
      responses:
        '200':
          description: Successful embedding response.
          headers:
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmbeddingResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ───────────────────────────────────────────────────────────────────
  # Models — accessible to the caller
  # ───────────────────────────────────────────────────────────────────
  /v1/auth/validate:
    get:
      tags: [Models]
      summary: Validate an API key
      operationId: validateApiKey
      description: |
        Echoes back the team, key name, environment and scopes attached to
        the calling key. No model is invoked and nothing is billed — the
        cheapest possible check that a deployment's credential is wired up.
      responses:
        '200':
          description: The key is valid.
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid: { type: boolean }
                  team_id: { type: string }
                  team_name: { type: string }
                  api_key_id: { type: string }
                  api_key_name: { type: string }
                  api_key_prefix: { type: string, example: sk_stav_live_ }
                  environment: { type: string, enum: [PRODUCTION, DEVELOPMENT] }
                  scopes:
                    type: array
                    items: { type: string }
                    description: |
                      Empty means no restriction — an unscoped key has full
                      access.
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/models:
    get:
      tags: [Models]
      summary: List models accessible to the caller
      operationId: listModels
      description: |
        Lists every model the authenticated key can invoke, filtered by
        key scope, team plan, and any team-level model allow/deny lists.
        Drop-in for the OpenAI SDK's `client.models.list()`.
      responses:
        '200':
          description: List of accessible models.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelList' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/models/{model_id}:
    get:
      tags: [Models]
      summary: Get metadata for a specific model
      operationId: retrieveModel
      parameters:
        - in: path
          name: model_id
          required: true
          schema: { type: string }
          description: |
            Catalogue model identifier, matched exactly.
            Example: `claude-sonnet-4-5-20250929`.
      responses:
        '200':
          description: Model metadata.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Model' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/ModelNotFound' }

  # ───────────────────────────────────────────────────────────────────
  # Catalogue — anonymous browse
  # ───────────────────────────────────────────────────────────────────
  /v1/public/models:
    get:
      tags: [Catalogue]
      summary: Browse the public model catalogue
      operationId: listCatalogueModels
      security: []  # anonymous
      description: |
        Lists every active model in the Stav catalogue. No auth required.
        ETag-cached. Default rate limit 60 req/min/IP.
      parameters:
        - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 }, description: Page number (1-based) }
        - { in: query, name: page_size, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
        - { in: query, name: category, schema: { type: string, enum: [llm, code, embedding, vision, speech, multimodal] } }
        - { in: query, name: creator_slug, schema: { type: string } }
        - { in: query, name: is_open_weights, schema: { type: boolean } }
        - { in: query, name: featured, schema: { type: boolean } }
        - { in: query, name: search, schema: { type: string } }
        - { in: query, name: model_family, schema: { type: string } }
        - { in: query, name: sort_by, schema: { type: string, enum: [sort_order, name, quality_index, arena_elo, parameter_count, created_at], default: sort_order } }
        - { in: query, name: sort_dir, schema: { type: string, enum: [asc, desc], default: asc } }
      responses:
        '200':
          description: Paginated catalogue list.
          headers:
            ETag: { schema: { type: string } }
            Cache-Control: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CatalogueModelList' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/public/models/{slug}:
    get:
      tags: [Catalogue]
      summary: Get a catalogue model by slug
      operationId: getCatalogueModel
      security: []
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Full model detail.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CatalogueModelDetail' }
        '404':
          description: Model not found.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/public/families:
    get:
      tags: [Catalogue]
      summary: List model families
      operationId: listFamilies
      security: []
      responses:
        '200':
          description: All active model families (groupings like Qwen3, Llama 4, Claude).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FamilyList' }

  /v1/public/families/{slug}:
    get:
      tags: [Catalogue]
      summary: Get a model family by slug
      operationId: getFamily
      security: []
      parameters:
        - { in: path, name: slug, required: true, schema: { type: string } }
      responses:
        '200':
          description: Family detail.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Family' }
        '404': { $ref: '#/components/responses/ModelNotFound' }

  /v1/public/families/{slug}/models:
    get:
      tags: [Catalogue]
      summary: List models in a family
      operationId: listFamilyModels
      security: []
      parameters:
        - { in: path, name: slug, required: true, schema: { type: string } }
      responses:
        '200':
          description: Models that belong to this family.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CatalogueModelList' }

  /v1/public/model-creators:
    get:
      tags: [Catalogue]
      summary: List model creators
      operationId: listCreators
      security: []
      description: |
        Returns every creator with at least one active catalogue model
        (Qwen, Meta, Mistral, OpenAI, Anthropic, DeepSeek, etc.).
      responses:
        '200':
          description: Creator list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreatorList' }

  /v1/public/model-creators/{slug}:
    get:
      tags: [Catalogue]
      summary: Get a model creator by slug
      operationId: getCreator
      security: []
      parameters:
        - { in: path, name: slug, required: true, schema: { type: string } }
      responses:
        '200':
          description: Creator detail with their model list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Creator' }
        '404': { $ref: '#/components/responses/ModelNotFound' }

  /v1/public/providers:
    get:
      tags: [Catalogue]
      summary: List inference providers
      operationId: listProviders
      security: []
      description: |
        Inference providers Stav routes through (Anthropic, OpenAI, Mistral
        La Plateforme, Google, xAI, Cohere and others), accessed via Stav's
        upstream relationships.
      parameters:
        - name: cada_min
          in: query
          required: false
          description: >
            Narrow to providers at or above a rung on the Stav Sovereignty
            Ladder — the procurement query, e.g. `cada_min=L3` for
            operators under EU/EEA ownership and control. Filters on the
            same effective level the rows report.
          schema: { type: string, enum: [L0, L1, L2, L3, L4] }
      responses:
        '200':
          description: Provider list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProviderList' }

  /v1/public/providers/{slug}:
    get:
      tags: [Catalogue]
      summary: Inference provider detail
      operationId: getProvider
      security: []
      description: |
        Full provider detail — sovereignty posture (Stav's assessment),
        the provider's own Trust Profile (legal documents +
        provenance-stamped certifications), the first-party Creator link
        when applicable, and the endpoints served.
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Provider detail.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProviderDetail' }
        '404':
          description: Provider not found.

  /v1/public/sovereignty:
    get:
      tags: [Catalogue]
      summary: Sovereignty levels
      operationId: listSovereigntyLevels
      security: []
      description: |
        The sovereignty ladder (levels L0–L4) and what each rung
        guarantees about data residency, operation, ownership & control,
        and audit trail.
      responses:
        '200':
          description: Sovereignty taxonomy.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SovereigntyList' }

  /v1/public/capabilities:
    get:
      tags: [Catalogue]
      summary: Capabilities taxonomy
      operationId: listCapabilities
      security: []
      description: |
        Capabilities catalogue (reasoning, tool-use, vision, code-gen,
        long-context, multilingual, etc.) used to tag models.
      responses:
        '200':
          description: Capability list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CapabilityList' }

# ─────────────────────────────────────────────────────────────────────
# Reusable components
# ─────────────────────────────────────────────────────────────────────
components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_stav_live_...
      description: |
        Stav API key. `sk_stav_live_<random>` (production) or
        `sk_stav_test_<random>` (sandbox); user-scoped keys carry a
        `sk_stav_user_` prefix. Legacy `hm_*` keys are no longer accepted.

        `x-api-key` is accepted as an alternative header for Anthropic-SDK
        compatibility; `Authorization` wins when both are present.

        Create a key at https://stav.ai/app/connect/keys.

  headers:
    RequestId:
      description: |
        Unique request identifier, identical to the completion `id`. Quote
        this when contacting support. Emitted on successful responses only.
      schema: { type: string, example: "chatcmpl-8f3a1c2e1b8d4a5f" }
    StavModel:
      description: |
        The model that actually served the request — not necessarily the one
        you asked for, when routing or failover intervened.
      schema: { type: string, example: "claude-sonnet-4-5-20250929" }
    StavProvider:
      description: The provider that served the request.
      schema: { type: string, example: anthropic }
    StavModelType:
      description: |
        How the model that served is classified. Commercial models proxied
        through Stav report `routed_commercial`.
      schema: { type: string, example: routed_commercial }
    StavRouteReason:
      description: |
        One-line, human-readable rationale for the selection. Present on
        pinned requests too, where it restates the selection. Prose —
        do not parse it; the structured decision is in the routing log.
      schema: { type: string, example: "Auto-routed: best score 78.4 based on team weights" }
    StavRouter:
      description: The named or default router that handled the request, when one did.
      schema: { type: string, example: "@acme/eu-only" }
    StavRouteTimeMs:
      description: |
        On a streaming response, time from handler start to provider dispatch.
        On a non-streaming response, total handler latency including the
        provider round-trip. The two are not comparable.
      schema: { type: string, example: "31.0" }
    SovereigntyLevel:
      description: |
        The effective sovereignty level (L0–L4 ladder) of the endpoint
        that served this request. `UNCLASSIFIED` means the serving
        endpoint has not been assessed yet (a data-quality state, not a
        rung). The same value is recorded on the request log entry.
      schema:
        type: string
        enum: [L0, L1, L2, L3, L4, UNCLASSIFIED]
    RateLimitLimit:
      description: Per-minute request cap on this key.
      schema: { type: integer }
    RateLimitRemaining:
      description: Requests remaining in the current window.
      schema: { type: integer }
    RateLimitReset:
      description: Unix timestamp (seconds) when the current window rolls over.
      schema: { type: integer, format: int64 }

  responses:
    BadRequest:
      description: Invalid request — malformed JSON, missing required fields, or invalid parameter values.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: API key lacks the required scope (e.g., `inference:messages`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ModelNotFound:
      description: Model id is not in the catalogue, or the caller's key doesn't have access to it.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Per-key or per-IP rate limit exceeded.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    UpstreamError:
      description: An upstream provider (Anthropic, OpenAI, etc.) returned an error.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:

    # ─── Error envelope ────────────────────────────────────────────
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [message, type]
          properties:
            type:
              type: string
              description: Coarse error category.
              enum:
                - invalid_request_error
                - authentication_error
                - permission_error
                - not_found_error
                - rate_limit_error
                - api_error
                - overloaded_error
            code:
              type: [string, 'null']
              description: Fine-grained machine-readable error code.
              example: model_not_available
            message:
              type: string
              description: Human-readable error message. May change without notice — match on `type`/`code`, not `message`.
            request_id:
              type: string
              description: |
                The identifier to quote when contacting support. Present on
                every error body, including the SSE error frame of a
                streaming request. The same value is on the
                `X-Stav-Request-Id` response header.
              example: chatcmpl-3f9c1a7b2e5d4086b1c9a2f7

    # ─── Chat completions (OpenAI-compatible) ──────────────────────
    ChatCompletionRequest:
      type: object
      required: [model, messages]
      properties:
        model:
          type: string
          description: |
            One of three forms:

            * a catalogue model slug, matched exactly (no alias resolution);
            * `auto` — your team's default routing policy;
            * `@team-slug/router-slug` — a named router your team authored.
          examples:
            - auto
            - "@acme/eu-only"
            - claude-sonnet-4-5-20250929
            - gpt-4o-mini
        messages:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/ChatMessage' }
        stream: { type: boolean, default: false }
        stream_options:
          type: object
          description: Streaming options, applied only when `stream=true`.
          properties:
            include_usage:
              type: boolean
              description: |
                When true, a final SSE chunk carries a `usage` object with
                token counts and an empty `choices` array, before the
                `[DONE]` sentinel. Without it a streaming response reports no
                token counts at all.
        n:
          type: integer
          minimum: 1
          default: 1
          description: |
            Number of choices to return. Only `1` is supported — `n>1`
            returns 400 (`unsupported_parameter`). Issue parallel requests
            instead.
        session_id:
          type: string
          maxLength: 128
          description: |
            Stav extension. Explicit conversation-session identifier;
            overrides response-anchored chaining. Echoed back as
            `X-Session-Id`.
        provider:
          type: object
          description: |
            Stav extension. Narrows which providers may serve an
            **explicitly pinned** model. Not applied on the `auto` or
            named-router path — shape those with team policy and router
            allowlists instead.
          properties:
            only:
              type: array
              items: { type: string }
              description: Provider allowlist.
            ignore:
              type: array
              items: { type: string }
              description: Provider blocklist.
            max_price:
              type: object
              description: Per-1M-token EUR ceiling.
              properties:
                prompt: { type: number }
                completion: { type: number }
            quantizations:
              type: array
              items: { type: string }

        # <<< GENERATED: registry parameters — do not edit by hand
        temperature:
          type: number
          minimum: 0
          maximum: 2
          description: |
            Sampling temperature. Higher is more random. Honoured on: OpenAI, Anthropic, Google, Mistral.
        top_p:
          type: number
          minimum: 0
          maximum: 1
          description: |
            Nucleus sampling cutoff. Honoured on: OpenAI, Anthropic, Google, Mistral.
        max_tokens:
          type: integer
          minimum: 1
          description: |
            Maximum tokens to generate. Honoured on: OpenAI, Anthropic, Google, Mistral.
        response_format:
          oneOf:
            - { $ref: '#/components/schemas/ResponseFormatText' }
            - { $ref: '#/components/schemas/ResponseFormatJsonObject' }
            - { $ref: '#/components/schemas/ResponseFormatJsonSchema' }
            - { $ref: '#/components/schemas/ResponseFormatGrammar' }
          description: |
            Structured-output contract (json_object / json_schema / grammar). Honoured on: OpenAI, Anthropic, Google, Mistral. Requires the `structured_output` capability — with `model="auto"` this becomes a hard filter on the candidate pool.
        tools:
          type: array
          items: { $ref: '#/components/schemas/Tool' }
          description: |
            Function/tool definitions the model may call. Honoured on: OpenAI, Anthropic, Google, Mistral. Requires the `function_calling` capability — with `model="auto"` this becomes a hard filter on the candidate pool.
        tool_choice:
          oneOf:
            - { type: string, enum: [none, auto, required] }
            - { $ref: '#/components/schemas/ToolChoiceFunction' }
          description: |
            Constrain which tool the model may call. Honoured on: OpenAI, Anthropic, Google, Mistral. Requires the `function_calling` capability — with `model="auto"` this becomes a hard filter on the candidate pool.
        reasoning_effort:
          type: string
          enum: [minimal, low, medium, high]
          description: |
            Reasoning depth for models that support extended thinking (minimal, low, medium, high). Honoured on: OpenAI, Anthropic, Google, Mistral. Aliases accepted: `reasoning`, `thinking`. Requires the `extended_thinking` capability — with `model="auto"` this becomes a hard filter on the candidate pool.
        stop:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
          description: |
            Up to 4 sequences that halt generation. Honoured on: OpenAI, Anthropic, Google, Mistral.
        top_k:
          type: integer
          minimum: 0
          description: |
            Top-k sampling cutoff. Honoured on: Anthropic, Google. Returns 400 `unsupported_parameter` on: OpenAI, Mistral.
        seed:
          type: integer
          description: |
            Best-effort determinism across identical requests. Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        logprobs:
          type: boolean
          description: |
            Return log probabilities for output tokens. Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        top_logprobs:
          type: integer
          minimum: 0
          maximum: 20
          description: |
            Most-likely tokens to return per position (0–20). Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        presence_penalty:
          type: number
          minimum: -2
          maximum: 2
          description: |
            Penalise tokens already present in the text. Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        frequency_penalty:
          type: number
          minimum: -2
          maximum: 2
          description: |
            Penalise tokens by existing frequency. Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        logit_bias:
          type: object
          additionalProperties: { type: integer, minimum: -100, maximum: 100 }
          description: |
            Per-token likelihood bias map (token id → −100..100). Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        verbosity:
          type: string
          enum: [low, medium, high]
          description: |
            GPT-5 output verbosity hint (low / medium / high). Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        prediction:
          type: object
          description: |
            Predicted output for latency-optimised edits (OpenAI). Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        prompt_cache_key:
          type: string
          description: |
            Cache-routing hint for OpenAI automatic prompt caching. Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        safety_identifier:
          type: string
          description: |
            Stable end-user identifier for abuse monitoring. Honoured on OpenAI; declared intentional-ignore elsewhere (portable clients send it universally). Honoured on: OpenAI. Declared no-op on: Anthropic, Google, Mistral (accepted and ignored, never a 400).
        service_tier:
          type: string
          enum: [auto, default, flex, priority]
          description: |
            OpenAI processing tier (auto / default / flex / priority). Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        user:
          type: string
          description: |
            Stable end-user identifier (legacy; see safety_identifier). Honoured on OpenAI; declared intentional-ignore elsewhere (OpenAI-SDK clients attach it to every request, any provider). Honoured on: OpenAI. Declared no-op on: Anthropic, Google, Mistral (accepted and ignored, never a 400).
        parallel_tool_calls:
          type: boolean
          description: |
            Allow the model to emit multiple tool calls in one turn. Honoured on: OpenAI. Returns 400 `unsupported_parameter` on: Anthropic, Google, Mistral.
        min_p:
          type: number
          minimum: 0
          maximum: 1
          description: |
            Min-p nucleus floor. Provider-specific tuning. Not honoured on any provider — returns 400 `unsupported_parameter` naming the parameter and the model that resolved.
        repetition_penalty:
          type: number
          description: |
            Repetition penalty. Provider-specific tuning. Not honoured on any provider — returns 400 `unsupported_parameter` naming the parameter and the model that resolved.
        store:
          type: boolean
          description: |
            Persist the completion for later retrieval. Stav has no retrieval store. Not honoured on any provider — returns 400 `unsupported_parameter` naming the parameter and the model that resolved.
        metadata:
          type: object
          additionalProperties: { type: string }
          description: |
            Free-form tags on a stored completion. Tied to `store`. Not honoured on any provider — returns 400 `unsupported_parameter` naming the parameter and the model that resolved.
        modalities:
          type: array
          items: { type: string }
          description: |
            Requested output modalities (e.g. audio). Stav serves text. Not honoured on any provider — returns 400 `unsupported_parameter` naming the parameter and the model that resolved.
        include_reasoning:
          type: boolean
          description: |
            Echo the reasoning trace in the response. Reasoning is reported as a token count on `usage.completion_tokens_details` instead. Not honoured on any provider — returns 400 `unsupported_parameter` naming the parameter and the model that resolved.
        chat_template_kwargs:
          type: object
          description: |
            Backend chat-template arguments. Not honoured on any provider — returns 400 `unsupported_parameter` naming the parameter and the model that resolved.
        # >>> END GENERATED

    ChatMessage:
      type: object
      required: [role]
      properties:
        role: { type: string, enum: [system, user, assistant, tool] }
        content:
          description: |
            String for text-only messages, or array of content parts
            (text, image_url, input_audio) for multimodal models.
          oneOf:
            - { type: string }
            - { type: array, items: { $ref: '#/components/schemas/ContentPart' } }
        name: { type: string, description: Optional name for the message author. }
        tool_call_id: { type: string, description: Required when role is `tool`. }
        tool_calls:
          type: array
          description: Tool calls the assistant made (assistant-role messages only).
          items: { $ref: '#/components/schemas/ToolCall' }

    ContentPart:
      oneOf:
        - type: object
          required: [type, text]
          properties:
            type: { type: string, const: text }
            text: { type: string }
            cache_control:
              type: object
              description: Anthropic-style cache marker. Honoured when the underlying model supports prompt caching.
              properties:
                type: { type: string, const: ephemeral }
        - type: object
          required: [type, image_url]
          properties:
            type: { type: string, const: image_url }
            image_url:
              type: object
              required: [url]
              properties:
                url: { type: string, description: "HTTPS URL or data:image/...;base64,... data URI." }
                detail: { type: string, enum: [auto, low, high], default: auto }

    Tool:
      description: |
        A function your application implements. Stav returns the call the
        model wants to make; you execute it and send the result back as a
        `tool` message. Stav does not execute tools server-side.
      allOf:
        - $ref: '#/components/schemas/FunctionTool'

    FunctionTool:
      type: object
      required: [type, function]
      properties:
        type: { type: string, const: function }
        function:
          type: object
          required: [name]
          properties:
            name: { type: string }
            description: { type: string }
            parameters: { description: JSON Schema for the function's arguments. }

    ToolChoiceFunction:
      type: object
      required: [type, function]
      properties:
        type: { type: string, const: function }
        function:
          type: object
          required: [name]
          properties:
            name: { 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, description: JSON-encoded argument object. }

    ResponseFormatText:
      type: object
      required: [type]
      properties:
        type: { type: string, const: text }

    ResponseFormatJsonObject:
      type: object
      required: [type]
      properties:
        type: { type: string, const: json_object }

    ResponseFormatJsonSchema:
      type: object
      required: [type, json_schema]
      properties:
        type: { type: string, const: json_schema }
        json_schema:
          type: object
          required: [name, schema]
          properties:
            name: { type: string }
            description: { type: string }
            strict: { type: boolean, default: false }
            schema: { description: A JSON Schema 2020-12 document. }

    ResponseFormatGrammar:
      type: object
      required: [type, grammar]
      description: |
        Stav extension — grammar-constrained decoding, for outputs that are
        not JSON. Requires the `structured_output` capability.
      properties:
        type: { type: string, const: grammar }
        grammar:
          type: object
          required: [syntax, pattern]
          properties:
            syntax: { type: string, enum: [regex, ebnf] }
            pattern: { type: string, description: The regular expression or EBNF grammar source. }

    ChatCompletionResponse:
      type: object
      required: [id, object, created, model, choices]
      properties:
        id: { type: string, example: "chatcmpl-abc123..." }
        object: { type: string, const: chat.completion }
        created: { type: integer, format: int64, description: Unix timestamp (seconds). }
        model: { type: string, description: The model id actually used (may differ from the request if `model="auto"` or a tier suffix was resolved). }
        choices:
          type: array
          items:
            type: object
            required: [index, message, finish_reason]
            properties:
              index: { type: integer }
              message: { $ref: '#/components/schemas/ChatMessage' }
              finish_reason: { type: string, enum: [stop, length, tool_calls, content_filter, function_call] }
              logprobs: { type: [object, 'null'] }
        usage: { $ref: '#/components/schemas/Usage' }
        system_fingerprint: { type: [string, 'null'] }

    Usage:
      type: object
      description: |
        Token counts for the request. What the routing decision was is
        reported in the `X-Stav-*` response headers, not in the body.
      properties:
        prompt_tokens: { type: integer }
        completion_tokens: { type: integer }
        total_tokens: { type: integer }
        completion_tokens_details:
          type: object
          description: |
            Present only when the model spent reasoning tokens.
          properties:
            reasoning_tokens:
              type: integer
              description: |
                The portion of `completion_tokens` spent on extended
                thinking. A breakdown, not an addition — `completion_tokens`
                already includes it. The thinking text itself is not returned
                on this surface.
        prompt_tokens_details:
          type: object
          description: Present only when the request read from a prompt cache.
          properties:
            cached_tokens:
              type: integer
              description: |
                The portion of `prompt_tokens` served from a prompt cache. A
                breakdown, not an addition. The cache *write* is billed
                separately and is not reported here.

    # ─── Messages (Anthropic-compatible) ───────────────────────────
    MessagesRequest:
      type: object
      required: [model, max_tokens, messages]
      properties:
        model: { type: string, examples: [claude-sonnet-4-5, claude-opus-4-6, qwen3-32b] }
        max_tokens: { type: integer, minimum: 1 }
        messages:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/AnthropicMessage' }
        system:
          oneOf:
            - { type: string }
            - { type: array, items: { $ref: '#/components/schemas/AnthropicContentBlock' } }
        temperature: { type: number, minimum: 0, maximum: 1 }
        top_p: { type: number, minimum: 0, maximum: 1 }
        top_k: { type: integer, minimum: 0 }
        stream: { type: boolean, default: false }
        stop_sequences: { type: array, items: { type: string } }
        tools:
          type: array
          items: { $ref: '#/components/schemas/AnthropicTool' }
        tool_choice:
          oneOf:
            - { type: object, properties: { type: { type: string, const: auto } } }
            - { type: object, properties: { type: { type: string, const: any } } }
            - { type: object, required: [type, name], properties: { type: { type: string, const: tool }, name: { type: string } } }
        thinking:
          type: object
          description: Extended thinking config (Anthropic models only).
          properties:
            type: { type: string, enum: [enabled, disabled] }
            budget_tokens: { type: integer }
        metadata:
          type: object
          properties:
            user_id: { type: string }

    AnthropicMessage:
      type: object
      required: [role, content]
      properties:
        role: { type: string, enum: [user, assistant] }
        content:
          oneOf:
            - { type: string }
            - { type: array, items: { $ref: '#/components/schemas/AnthropicContentBlock' } }

    AnthropicContentBlock:
      oneOf:
        - type: object
          required: [type, text]
          properties:
            type: { type: string, const: text }
            text: { type: string }
            cache_control:
              type: object
              properties:
                type: { type: string, const: ephemeral }
        - type: object
          required: [type, source]
          properties:
            type: { type: string, const: image }
            source:
              type: object
              required: [type, media_type, data]
              properties:
                type: { type: string, enum: [base64, url] }
                media_type: { type: string, examples: [image/jpeg, image/png, image/webp, image/gif] }
                data: { type: string }
        - type: object
          required: [type, id, name, input]
          properties:
            type: { type: string, const: tool_use }
            id: { type: string }
            name: { type: string }
            input: { description: JSON object — the tool call arguments. }
        - type: object
          required: [type, tool_use_id, content]
          properties:
            type: { type: string, const: tool_result }
            tool_use_id: { type: string }
            content:
              oneOf:
                - { type: string }
                - { type: array, items: { $ref: '#/components/schemas/AnthropicContentBlock' } }
            is_error: { type: boolean, default: false }

    AnthropicTool:
      type: object
      required: [name, input_schema]
      properties:
        name: { type: string }
        description: { type: string }
        input_schema: { description: JSON Schema 2020-12 document for the tool's input. }
        cache_control:
          type: object
          properties:
            type: { type: string, const: ephemeral }

    MessagesResponse:
      type: object
      required: [id, type, role, content, model, stop_reason, usage]
      properties:
        id: { type: string, example: msg_01XYZ... }
        type: { type: string, const: message }
        role: { type: string, const: assistant }
        content:
          type: array
          items: { $ref: '#/components/schemas/AnthropicContentBlock' }
        model: { type: string }
        stop_reason:
          type: string
          enum: [end_turn, max_tokens, stop_sequence, tool_use]
        stop_sequence: { type: [string, 'null'] }
        usage:
          type: object
          properties:
            input_tokens: { type: integer }
            output_tokens: { type: integer }
            cache_creation_input_tokens: { type: integer }
            cache_read_input_tokens: { type: integer }

    # ─── Embeddings ────────────────────────────────────────────────
    EmbeddingRequest:
      type: object
      required: [model, input]
      properties:
        model: { type: string, examples: [bge-m3, jina-embeddings-v3, mxbai-embed-large] }
        input:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string }, maxItems: 2048 }
        encoding_format: { type: string, enum: [float, base64], default: float }
        dimensions: { type: integer, description: "Truncate to this many dimensions (Matryoshka-style). Only honoured by models that support truncation." }
        user: { type: string }

    EmbeddingResponse:
      type: object
      required: [object, data, model, usage]
      properties:
        object: { type: string, const: list }
        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-encoded float32 array when `encoding_format=base64`. }
        model: { type: string }
        usage:
          type: object
          properties:
            prompt_tokens: { type: integer }
            total_tokens: { type: integer }

    ModelList:
      type: object
      required: [object, data]
      properties:
        object: { type: string, const: list }
        data:
          type: array
          items: { $ref: '#/components/schemas/Model' }

    Model:
      type: object
      required: [id, object, created, owned_by]
      description: |
        OpenAI-compatible model object. The `stav_*` fields are additive
        extensions — the OpenAI SDK ignores them, and they are how a client
        discovers what a model honours before sending a request.
      properties:
        id: { type: string, example: claude-sonnet-4-5-20250929 }
        object: { type: string, const: model }
        created: { type: integer, format: int64 }
        owned_by: { type: string, example: anthropic }
        stav_category: { type: [string, 'null'], example: llm }
        stav_context_window: { type: [integer, 'null'], description: Max context tokens. }
        stav_parameters: { type: [string, 'null'] }
        stav_quantization: { type: [string, 'null'] }
        stav_supports_thinking:
          type: [boolean, 'null']
          description: Whether the model honours `reasoning_effort` / extended thinking.
        stav_capabilities:
          type: [object, 'null']
          additionalProperties: { type: boolean }
          description: |
            Capability flags a client can rely on before sending — e.g.
            `extended_thinking`, `tool_use`, `vision`, `structured_output`,
            `pdf_input`. These are the same flags the Smart Router
            hard-filters on.

    # ─── Catalogue ─────────────────────────────────────────────────
    CatalogueModelList:
      type: object
      required: [items, total, page, page_size]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/CatalogueModelSummary' }
        total: { type: integer }
        page: { type: integer }
        page_size: { type: integer }

    CatalogueModelSummary:
      type: object
      required: [slug, name, creator_slug, category]
      properties:
        slug: { type: string }
        name: { type: string }
        creator_slug: { type: string }
        creator_name: { type: string }
        family_slug: { type: [string, 'null'] }
        category: { type: string, enum: [llm, code, embedding, vision, speech, multimodal] }
        is_open_weights: { type: boolean }
        parameter_count: { type: [integer, 'null'] }
        context_window: { type: [integer, 'null'] }
        quality_index: { type: [number, 'null'] }
        arena_elo: { type: [integer, 'null'] }
        featured: { type: boolean }
        price_per_million_input_tokens_eur: { type: [number, 'null'] }
        price_per_million_output_tokens_eur: { type: [number, 'null'] }

    CatalogueModelDetail:
      allOf:
        - $ref: '#/components/schemas/CatalogueModelSummary'
        - type: object
          properties:
            description: { type: string }
            curated_description: { type: [string, 'null'] }
            capabilities:
              type: array
              items: { type: string }
            benchmarks:
              type: array
              items:
                type: object
                properties:
                  slug: { type: string }
                  name: { type: string }
                  score: { type: number }
                  measured_at: { type: string, format: date-time }
            release_date: { type: [string, 'null'], format: date }

    FamilyList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Family' }

    Family:
      type: object
      required: [slug, name, creator_slug]
      properties:
        slug: { type: string }
        name: { type: string }
        creator_slug: { type: string }
        description: { type: string }
        model_count: { type: integer }

    CreatorList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Creator' }

    Creator:
      type: object
      required: [slug, name]
      properties:
        slug: { type: string }
        name: { type: string }
        country: { type: [string, 'null'] }
        description: { type: [string, 'null'] }
        logo_url: { type: [string, 'null'] }
        model_count: { type: integer }

    ProviderList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Provider' }

    Provider:
      type: object
      required: [slug, name, sovereignty]
      properties:
        slug: { type: string }
        name: { type: string }
        sovereignty: { type: string, enum: [sovereign, eu_hosted, non_eu], description: Legacy 3-lane value (transition alias — use sovereignty_level) }
        sovereignty_level: { type: [string, 'null'], enum: [L0, L1, L2, L3, L4, null], description: Stav sovereignty level on the L0–L4 ladder }
        jurisdiction: { type: [string, 'null'], enum: [eea, eu, us, cn, other, null] }
        cloud_act_exposure: { type: boolean }
        data_residency: { type: [string, 'null'], enum: [eea, eu, global, unknown, null] }
        country: { type: [string, 'null'] }
        country_name: { type: [string, 'null'] }
        region: { type: [string, 'null'] }
        endpoint_count: { type: [integer, 'null'] }
        is_first_party: { type: [boolean, 'null'] }

    ProviderCertification:
      type: object
      required: [name, status]
      properties:
        name: { type: string, example: ISO 27001 }
        scope: { type: [string, 'null'] }
        status: { type: string, enum: [current, expired, in_progress, not_held] }
        assurance_level:
          type: [string, 'null']
          enum: [none, basic, substantial, high, null]
          description: >
            Assurance level of the certificate, when known. Feeds the
            sovereignty-ladder criterion l2.security_certified (at or above
            substantial). L4 keys off a sovereign qualification such as
            SecNumCloud, not an assurance tier.
        source_url: { type: [string, 'null'] }
        verified_at: { type: [string, 'null'], description: 'ISO date (YYYY-MM-DD)' }

    ProviderDetail:
      type: object
      required: [provider, endpoints]
      description: >
        Full provider detail with its Trust Profile (the provider's own
        published legal documents + certifications, curated and
        provenance-first) and the list of endpoints it serves.
      properties:
        provider:
          type: object
          properties:
            slug: { type: string }
            name: { type: string }
            sovereignty: { type: string, enum: [sovereign, eu_hosted, non_eu], description: Legacy 3-lane value (transition alias — use sovereignty_level) }
            sovereignty_level: { type: [string, 'null'], enum: [L0, L1, L2, L3, L4, null], description: Stav sovereignty level on the L0–L4 ladder }
            jurisdiction: { type: [string, 'null'] }
            cloud_act_exposure: { type: boolean }
            data_residency: { type: [string, 'null'] }
            country: { type: [string, 'null'] }
            country_name: { type: [string, 'null'] }
            region: { type: [string, 'null'] }
            description: { type: [string, 'null'] }
            website_url: { type: [string, 'null'] }
            endpoint_count: { type: integer }
            terms_url: { type: [string, 'null'] }
            privacy_policy_url: { type: [string, 'null'] }
            dpa_url: { type: [string, 'null'] }
            subprocessors_url: { type: [string, 'null'] }
            certifications:
              type: [array, 'null']
              items: { $ref: '#/components/schemas/ProviderCertification' }
            trust_profile_verified_at: { type: [string, 'null'] }
            first_party_creator:
              type: [object, 'null']
              properties:
                slug: { type: string }
                name: { type: string }
        compliance:
          type: [object, 'null']
          description: >
            Stav's serving-side compliance assessment of the operator —
            Stav's own judgement (score, verdict, dimension breakdown,
            severity-banded risk items, safeguards, privacy-policy issues,
            and the assessment narrative). Distinct from the provider's
            own Trust Profile facts above. Null until assessed. Exposed on
            the detail endpoint only — the providers index omits it.
          properties:
            score:
              type: [integer, 'null']
              description: Headline 0–100 serving-side composite.
            verdict:
              type: [string, 'null']
              enum: [trusted, conditional, elevated_risk, high_risk, null]
            dimensions:
              type: array
              description: >
                Five serving-side dimensions — serving_residency,
                legal_exposure, serving_retention, security_posture,
                contractual_posture.
              items:
                type: object
                properties:
                  slug: { type: string }
                  label: { type: string }
                  score: { type: integer }
                  note: { type: [string, 'null'] }
            risk_assessment:
              type: [object, 'null']
              description: '{overall_risk, signal_count, categories[]}'
            safeguards:
              type: [array, 'null']
              items: { type: object }
            privacy_policy_issues:
              type: [array, 'null']
              items: { type: object }
            body:
              type: [string, 'null']
              description: Stav's serving-side assessment narrative.
            verified_at:
              type: [string, 'null']
              description: Last human sign-off of the assessment.
        sovereignty_ladder:
          type: [object, 'null']
          description: >
            The criterion grid behind the provider's sovereignty level —
            why it sits where it does, and what it would take to move up.
            The full grid is returned, including criteria above the gated
            level. Null until a grid has been authored; the provider still
            carries a level in that case, derived from its coarse
            residency classification rather than from evidence. Stav's
            read is indicative and never an official recognition.
          properties:
            level:
              type: [string, 'null']
              enum: [L0, L1, L2, L3, L4, null]
              description: The gated rung — the highest rung whose every criterion up to it is met.
            registry_version:
              type: [string, 'null']
              description: >
                Criterion-registry version the grid was judged against.
                Returned so a level stays interpretable after the criteria
                change.
            summary: { type: [string, 'null'] }
            next_rung_hint:
              type: [string, 'null']
              description: One line on what stands between this provider and the next rung.
            verified_at: { type: [string, 'null'] }
            disclaimer: { type: string }
            signals:
              type: object
              description: >
                Supporting facts the criteria draw on (sbom, eu_owned,
                eu_controlled, eu_personnel, source_code_audit,
                effective_control, third_country_components), each with a
                status and optional source.
              additionalProperties: true
            rungs:
              type: array
              items:
                type: object
                properties:
                  level: { type: string, enum: [L1, L2, L3, L4] }
                  name: { type: [string, 'null'] }
                  summary: { type: [string, 'null'] }
                  cleared:
                    type: boolean
                    description: True when every criterion on the rung is met.
                  criteria:
                    type: array
                    items:
                      type: object
                      properties:
                        key: { type: string, example: l1.data_in_union }
                        label: { type: [string, 'null'] }
                        requirement: { type: [string, 'null'] }
                        status:
                          type: string
                          enum: [met, partial, unmet, unknown]
                          description: >
                            Only "met" clears a rung. "unknown" means not
                            yet researched and is deliberately distinct
                            from "unmet".
                        source_url: { type: [string, 'null'] }
                        note: { type: [string, 'null'] }
                        evidence: { type: [object, 'null'] }
        endpoints:
          type: array
          items:
            type: object
            properties:
              model_slug: { type: [string, 'null'] }
        endpoint_count: { type: integer }

    SovereigntyList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            required: [level, label, description]
            properties:
              level:
                type: string
                enum: [L0, L1, L2, L3, L4]
              label: { type: string, example: "Sovereign" }
              description: { type: string }

    CapabilityList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            required: [slug, name]
            properties:
              slug: { type: string }
              name: { type: string }
              description: { type: string }

# External docs link surfaces at the renderer top.
externalDocs:
  description: Narrative documentation — quickstart, concepts, recipes
  url: https://stav.ai/docs
