openapi: 3.0.3
info:
  title: Usage API
  version: 1.0.0
  description: >
    Billing-grade usage metering. One append-only usage-meter row is recorded
    per completed LLM generation, capturing the provider's reported token
    counts — input, output, cached, and reasoning tokens — attributed to the
    project, agent, and generation. Rows are immutable, so historical usage
    never changes after the fact.
  contact:
    name: SOAT API Support

servers:
  - url: '{baseUrl}'
    description: Base URL of your SOAT deployment (e.g. https://your-soat.com or http://localhost:5047)
    variables:
      baseUrl:
        description: The base URL of your SOAT deployment
        default: http://localhost:5047

tags:
  - name: Usage
    description: Inspect LLM token-usage meter rows

security:
  - bearerAuth: []

paths:
  /api/v1/usage/meters:
    get:
      tags:
        - Usage
      summary: List usage meters
      description: >
        Returns the raw usage-meter rows the caller can access, most recent
        first, optionally filtered by agent, generation, trace, actor, session,
        or `source`. Each row is the per-generation token usage as reported by
        the provider, for audit and reconciliation.
      operationId: listUsageMeters
      parameters:
        - name: agent_id
          in: query
          required: false
          description: Filter by agent public ID
          schema:
            type: string
        - name: generation_id
          in: query
          required: false
          description: Filter by generation public ID
          schema:
            type: string
        - name: trace_id
          in: query
          required: false
          description: Filter by trace public ID
          schema:
            type: string
        - name: actor_id
          in: query
          required: false
          description: >
            Filter by the actor (end user) the usage is attributed to. An actor
            that does not exist in scope yields an empty page.
          schema:
            type: string
        - name: session_id
          in: query
          required: false
          description: >
            Filter by the session the usage was produced in. A session that does
            not exist in scope yields an empty page.
          schema:
            type: string
        - name: trigger_id
          in: query
          required: false
          description: Filter by the trigger that initiated the generation
          schema:
            type: string
        - name: action_id
          in: query
          required: false
          description: Filter by logical action id
          schema:
            type: string
        - name: meter_type
          in: query
          required: false
          description: >
            Filter by meter type (e.g. `llm_tokens`, `compute_execution`,
            `api_request`, `storage`)
          schema:
            type: string
        - name: source
          in: query
          required: false
          description: >
            Filter by what the spend was incurred for. `eval` is an eval run's
            item generations and `eval_judge` an `llm_judge` scorer's own
            completion, so verification spend is `source` in (`eval`,
            `eval_judge`). Ordinary agent traffic carries no source and is
            matched by neither.
          schema:
            type: string
            example: eval
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated list of usage-meter rows
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UsageEvent'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/usage:
    get:
      tags:
        - Usage
      summary: Get aggregated usage for a project
      description: >
        Returns a project's usage rolled up over an optional `[from, to]` time
        window, bucketed by a single dimension and optionally narrowed to one
        `meter_type`. Each group and the grand total carry summed token counts, a
        measured `quantity` per component (so infra meters report their real
        amount, not zeros), and `cost_usd` (null when no event in the bucket was
        priced). This is the per-project cost-by-range/by-category query — a
        monthly figure without scanning raw meter rows client-side.
      operationId: getUsage
      parameters:
        - name: project_id
          in: query
          required: true
          description: Project public ID to aggregate usage for
          schema:
            type: string
        - name: group_by
          in: query
          required: true
          description: >
            Dimension to bucket by. `day` buckets on the event's UTC calendar
            day; the others bucket on the matching column. `ai_provider` buckets
            on the provider the spend was billed against — a routed
            generation's serving target, or the agent's pinned provider.
            `model` buckets on the
            model id *and* the provider that served it, so one model name served
            by two providers is two groups (see `ai_provider_id`). `source` buckets by
            what the spend was incurred for (`eval`, `eval_judge`), which is how
            verification spend is priced apart from the traffic serving real
            users; unlabelled traffic collapses into the single `null` bucket.
          schema:
            type: string
            enum:
              [
                model,
                ai_provider,
                agent,
                run,
                day,
                meter_type,
                actor,
                session,
                source,
              ]
        - name: from
          in: query
          required: false
          description: >
            Inclusive lower bound (ISO-8601 timestamp) on the event created_at.
            Omit for no lower bound.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: >
            Inclusive upper bound (ISO-8601 timestamp) on the event created_at.
            Omit for no upper bound.
          schema:
            type: string
            format: date-time
        - name: meter_type
          in: query
          required: false
          description: >
            Narrow the rollup to one meter type (e.g. `llm_tokens`,
            `compute_execution`, `api_request`, `storage`). Omit to include
            every meter. An unknown type yields an empty rollup, not an error.
          schema:
            type: string
      responses:
        '200':
          description: The aggregated usage rollup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageAggregate'
        '400':
          description: Bad Request (missing project_id, invalid group_by or timestamp)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/usage/thresholds:
    get:
      tags:
        - Usage
      summary: List usage thresholds
      description: >
        Lists the usage alert thresholds the caller can access, optionally
        filtered by project_id. Each threshold fires the
        `usage.threshold_crossed` webhook when a project's cost or token usage
        over a calendar-month or rolling-24h window crosses the configured
        value.
      operationId: listUsageThresholds
      parameters:
        - name: project_id
          in: query
          required: false
          description: Filter by project public ID
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: The usage thresholds
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UsageThreshold'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - Usage
      summary: Create a usage threshold
      description: >
        Creates a usage alert threshold on a project. Thresholds are immutable
        apart from deletion — to change one, delete and recreate it (which
        resets its fire state).
      operationId: createUsageThreshold
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUsageThresholdRequest'
      responses:
        '201':
          description: The created threshold
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageThreshold'
        '400':
          description: Bad Request (missing/invalid metric, window, or threshold)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/usage/thresholds/{threshold_id}:
    delete:
      tags:
        - Usage
      summary: Delete a usage threshold
      description: >
        Deletes a usage threshold, resetting its fire state. Recreating a
        threshold starts its once-per-window / hysteresis state fresh.
      operationId: deleteUsageThreshold
      parameters:
        - name: threshold_id
          in: path
          required: true
          description: Threshold public ID
          schema:
            type: string
      responses:
        '204':
          description: Deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Threshold not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/usage/receipt:
    get:
      tags:
        - Usage
      summary: Get a generation or run billing receipt
      description: >
        Returns a billing receipt. Pass generation_id for a per-generation
        receipt, or orchestration_run_id for a per-run receipt summed across the orchestration
        run's meters — both share the same shape (per-model line items with
        tokens, the price-book version that priced them, and cost, plus totals).
        Exactly one of generation_id or orchestration_run_id must be supplied.
      operationId: getUsageReceipt
      parameters:
        - name: generation_id
          in: query
          required: false
          description: >
            Generation public ID. Mutually exclusive with orchestration_run_id.
          schema:
            type: string
        - name: orchestration_run_id
          in: query
          required: false
          description: >
            Orchestration run public ID. Returns the receipt summed across every
            generation the run metered. Mutually exclusive with generation_id.
          schema:
            type: string
      responses:
        '200':
          description: The generation or run usage receipt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageReceipt'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Generation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/usage/prices:
    get:
      tags:
        - Usage
      summary: Get the price book
      description: >
        Returns the global price book — the versioned per-provider/model unit
        prices used to compute usage cost at write time. Readable by any
        authenticated user.
      operationId: getPriceBook
      responses:
        '200':
          description: The current price book
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceBookResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    put:
      tags:
        - Usage
      summary: Upsert price-book rows
      description: >
        Upserts price rows keyed on (provider, model, effective_from). Admin
        only. `effective_from` must be in the future once the
        (provider, model, component) is priced — past prices are immutable so
        recorded costs stay explainable; ship corrections as new future-dated
        rows. A first price, with no row in this scope or any broader one, may
        be dated now or earlier.
      operationId: upsertPriceBook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertPricesRequest'
      responses:
        '200':
          description: The upserted price rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceBookResponse'
        '400':
          description: Bad Request (e.g. non-future effective_from)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    UsageComponent:
      type: object
      description: >
        One priced dimension of a usage event. Every meter type is expressed as
        components, so tokens and infra are uniform: an `llm_tokens` event has
        `input_tokens` / `output_tokens` (+ `cached_tokens`, and a non-billable
        `reasoning_tokens` detail), a `compute_execution` event has one
        `compute_second` component.
      properties:
        component:
          type: string
          description: The measured dimension (e.g. `input_tokens`, `compute_second`)
        quantity:
          type: number
          description: The measured amount, expressed in `unit`
        unit:
          type: string
          description: Unit `quantity` is measured in (`token`, `compute_second`, …)
        billable:
          type: boolean
          description: >
            Whether this component contributes to cost. Non-billable details
            (e.g. `reasoning_tokens`, a subset of `output_tokens`) are never
            priced and never double-counted into billable totals.
        unit_price:
          type: number
          nullable: true
          description: USD per `unit`, frozen at write time; null when unpriced
        cost_usd:
          type: number
          nullable: true
          description: quantity × unit_price, frozen at write time; null when unpriced
        price_id:
          type: string
          nullable: true
          description: Public ID of the price-book row that priced this component

    UsageEvent:
      type: object
      description: >
        One metered occurrence (a completed LLM call, a node execution, …).
        Attribution and total cost live here; the measured quantities live in
        `components`.
      properties:
        id:
          type: string
          description: Public ID of the usage event
          example: ue_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project the usage is attributed to
        orchestration_run_id:
          type: string
          nullable: true
          description: >
            Public ID of the orchestration run that initiated the occurrence.
            Null for standalone events.
        node_id:
          type: string
          nullable: true
          description: Orchestration node ID within the run, when applicable
        agent_id:
          x-soat-ref: agents
          type: string
          nullable: true
          description: Public ID of the agent that ran the generation
        generation_id:
          x-soat-ref: generations
          type: string
          nullable: true
          description: Public ID of the generation this usage was recorded for
        trace_id:
          x-soat-ref: traces
          type: string
          nullable: true
          description: Public ID of the trace this usage belongs to
        actor_id:
          x-soat-ref: actors
          type: string
          nullable: true
          description: >
            Public ID of the actor (end user) the metered occurrence was
            produced for, frozen at write time. Null when no end user is behind
            the work — orchestration runs, triggers, direct API generations.
        session_id:
          x-soat-ref: sessions
          type: string
          nullable: true
          description: >
            Public ID of the session the metered occurrence ran in, frozen at
            write time. Null for work not dispatched through a session.
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >
            Public ID of the AI provider instance billed — the target a model
            route picked for the turn, or the agent's pinned provider. Null if
            the provider was later deleted; the `provider`/`model` snapshot
            still records what was billed.
        trigger_id:
          x-soat-ref: triggers
          type: string
          nullable: true
          description: >
            Public ID of the trigger that initiated the generation (agent-target
            triggers). Null when not started by a trigger.
        action_id:
          type: string
          nullable: true
          description: >
            Caller-supplied logical action label, for rolling spend up per
            action. Null when the caller did not label the action.
        meter_type:
          type: string
          description: >
            What the event measures: `llm_tokens`, `compute_execution`,
            `api_request`, or `storage`.
        source:
          type: string
          nullable: true
          description: >
            What the spend was incurred for. `eval` is an eval run's item
            generations and `eval_judge` an `llm_judge` scorer's own completion,
            so verification spend is `source` in (`eval`, `eval_judge`) and the
            two are distinct so running a suite can be priced apart from grading
            it. Null for ordinary agent traffic.
          example: eval
        provider:
          type: string
          description: >
            As-billed SKU vendor slug, retained even if the AI provider is
            deleted. `soat` for platform meter types.
          example: openai
        model:
          type: string
          description: Model id, or the billable SKU for platform meter types
          example: gpt-4o
        cost_usd:
          type: number
          nullable: true
          description: >
            Total USD cost — the sum of the priced component costs, frozen at
            write time. Null when no component was priced.
        components:
          type: array
          items:
            $ref: '#/components/schemas/UsageComponent'
        created_at:
          type: string
          format: date-time

    UsageAggregateComponent:
      type: object
      description: >
        One measured dimension of a bucket, summed across its events. This is
        what makes the rollup uniform over meter types: the token fields only
        describe `llm_tokens`, so an infra meter's amount is read here.
      properties:
        component:
          type: string
          description: >
            The measured dimension: `input_tokens`, `cached_tokens`,
            `output_tokens`, `reasoning_tokens`, `compute_second`, `request`,
            `gb_day`, …
        unit:
          type: string
          description: >
            Unit `quantity` is measured in: `token`, `compute_second`,
            `request`, `gb_day`.
        quantity:
          type: number
          description: >
            Summed measured amount in `unit`. Non-integer for fractional
            measures (GB-days, compute seconds).
          example: 0.4
        cost_usd:
          type: number
          nullable: true
          description: >
            Sum of the priced component costs; null when no price row covered
            the component — the quantity is still captured, it does not mean the
            usage was free.

    UsageAggregateTotals:
      type: object
      description: >
        Summed token counts, measured component quantities, and cost for a
        bucket (or the grand total).
      properties:
        cost_usd:
          type: number
          nullable: true
          description: Sum of priced event costs in the bucket; null when nothing is priced
        input_tokens:
          type: integer
          description: Full prompt tokens (uncached input + cached), reconstructed from components
        output_tokens:
          type: integer
        cached_tokens:
          type: integer
        reasoning_tokens:
          type: integer
        components:
          type: array
          description: >
            Every component measured in the bucket, sorted by `component` then
            `unit`. The token fields above cover `llm_tokens` only, so this is
            where a `storage` / `api_request` / `compute_execution` bucket
            reports what it actually measured.
          items:
            $ref: '#/components/schemas/UsageAggregateComponent'

    UsageAggregate:
      type: object
      properties:
        project_id:
          x-soat-ref: projects
          type: string
        from:
          type: string
          format: date-time
          nullable: true
          description: Lower bound applied, echoed back; null when unbounded
        to:
          type: string
          format: date-time
          nullable: true
          description: Upper bound applied, echoed back; null when unbounded
        group_by:
          type: string
          enum:
            [
              model,
              ai_provider,
              agent,
              run,
              day,
              meter_type,
              actor,
              session,
              source,
            ]
        meter_type:
          type: string
          nullable: true
          description: Meter-type filter applied, echoed back; null when unfiltered
        groups:
          type: array
          description: One entry per distinct value in the chosen dimension.
          items:
            allOf:
              - type: object
                properties:
                  key:
                    type: string
                    nullable: true
                    description: >
                      The bucket's value in the chosen dimension (a model id,
                      meter type, AI provider / agent / run / actor / session
                      public id, or `YYYY-MM-DD` UTC day). Null when the dimension does not
                      apply to an event — under `actor`/`session` this is the
                      bucket holding everything with no end user behind it.
                  ai_provider_id:
                    type: string
                    x-soat-ref: ai-providers
                    nullable: true
                    description: >
                      Under `group_by=model`, the provider that served the
                      bucket's model; null on every other dimension. A model id
                      does not identify its provider on its own — one project
                      can hold two providers serving byte-identical model names
                      — so the model dimension buckets on (model,
                      `ai_provider_id`) and two groups may repeat one `key`
                      with different providers. The groups still sum to
                      `totals`.
              - $ref: '#/components/schemas/UsageAggregateTotals'
        totals:
          $ref: '#/components/schemas/UsageAggregateTotals'

    UsageThreshold:
      type: object
      description: >
        A per-project alert rule on windowed usage. When the project's `metric`
        over `window` crosses `threshold`, a `usage.threshold_crossed` webhook
        fires. Fire state is enforced with once-per-window (calendar) / 10%
        re-arm (rolling) hysteresis.
      properties:
        id:
          type: string
          description: Public ID of the threshold
          example: uthr_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        metric:
          type: string
          enum: [cost_usd, tokens]
          description: >
            What is measured: `cost_usd` (across all meter types) or `tokens`
            (input + output + cached).
        window:
          type: string
          enum: [calendar_month, rolling_24h]
          description: >
            The evaluation window: the current UTC calendar month, or the
            trailing 24 hours.
        threshold:
          type: number
          description: The value the windowed aggregate must cross to fire
        last_fired_at:
          type: string
          format: date-time
          nullable: true
          description: When the threshold last fired; null until first fire
        fired_window_key:
          type: string
          nullable: true
          description: >
            The `YYYY-MM` window key of the last fire (calendar_month
            hysteresis); null for rolling_24h and before the first fire.
        created_at:
          type: string
          format: date-time

    CreateUsageThresholdRequest:
      type: object
      required:
        - project_id
        - metric
        - window
        - threshold
      properties:
        project_id:
          x-soat-ref: projects
          type: string
        metric:
          type: string
          enum: [cost_usd, tokens]
        window:
          type: string
          enum: [calendar_month, rolling_24h]
        threshold:
          type: number
          description: Must be greater than 0

    UsageReceipt:
      type: object
      properties:
        generation_id:
          description: >
            Present on a per-generation receipt; absent on a per-run receipt.
          x-soat-ref: generations
          type: string
        orchestration_run_id:
          description: >
            Present on a per-run receipt (summed across the run's meters); absent
            on a per-generation receipt.
          x-soat-ref: orchestration-runs
          type: string
        currency:
          type: string
          example: USD
        line_items:
          type: array
          description: >
            One line per usage event — for a generation receipt, the events on
            that generation; for a run receipt, every event across the run.
          items:
            type: object
            properties:
              event_id:
                type: string
              meter_type:
                type: string
              provider:
                type: string
              model:
                type: string
              node_id:
                type: string
                nullable: true
                description: >
                  Orchestration node that produced the event. On a run receipt
                  every line carries it — an `agent` node's `llm_tokens` line and
                  the `compute_execution` line of each node execution alike — so
                  grouping the lines by `node_id` gives the per-node cost the
                  receipt total alone hides. A retried node contributes one line
                  per attempt, all under the same `node_id` — the event records no
                  attempt number — which is the intended reading for spend: a
                  retry is real money. Null when no node produced the event (a
                  standalone generation, a run-level meter).
              cost_usd:
                type: number
                nullable: true
              components:
                type: array
                items:
                  $ref: '#/components/schemas/UsageComponent'
        by_meter_type:
          type: array
          description: >
            Per-meter-type cost rollup — the "tokens + infra" split. A
            single-type receipt has one entry whose cost equals the receipt
            total.
          items:
            type: object
            properties:
              meter_type:
                type: string
              cost_usd:
                type: number
                nullable: true
        total_input_tokens:
          type: integer
          description: Full prompt tokens (uncached input + cached), reconstructed from components
        total_output_tokens:
          type: integer
        total_cached_tokens:
          type: integer
        total_reasoning_tokens:
          type: integer
        total_cost_usd:
          type: number
          nullable: true
          description: Sum of priced component costs; null when nothing is priced

    Price:
      type: object
      description: >
        A versioned unit price for one billable component of a SKU. Cost is
        uniform across meter types — quantity × unit_price.
      properties:
        id:
          type: string
          description: Public ID of the price row
          example: price_V1StGXR8Z5jdHi6B
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >
            Null for a global default price. Set when this row is a
            per-provider override for a specific AI provider instance.
        meter_type:
          type: string
          description: Meter type this SKU belongs to (`llm_tokens`, `compute_execution`, …)
        provider:
          type: string
          description: SKU vendor slug (`openai`, or `soat` for platform SKUs)
          example: openai
        model:
          type: string
          description: Model id, or the billable SKU for platform meter types
          example: gpt-4o
        component:
          type: string
          description: The component this row prices (`input_tokens`, `compute_second`, …)
        unit:
          type: string
          description: Unit `unit_price` is denominated in (`token`, `compute_second`, …)
        unit_price:
          type: number
          description: USD per `unit` (for token components, USD per token)
        effective_from:
          type: string
          format: date-time
          description: The row with the latest effective_from <= now() prices a call
        created_at:
          type: string
          format: date-time

    PriceBookResponse:
      type: object
      properties:
        prices:
          type: array
          items:
            $ref: '#/components/schemas/Price'

    UpsertPricesRequest:
      type: object
      required:
        - prices
      properties:
        prices:
          type: array
          items:
            type: object
            required:
              - provider
              - model
              - component
              - unit
              - unit_price
              - effective_from
            description: >
              A price row for one component of a SKU. For LLM SKUs supply the
              `input_tokens` / `output_tokens` (and optionally `cached_tokens`)
              components with unit `token`; for platform SKUs supply the
              platform component (e.g. `compute_second`).
            properties:
              ai_provider_id:
                x-soat-ref: ai-providers
                type: string
                nullable: true
                description: >
                  Omit for a global default price; set to a provider public ID
                  to record a per-provider override.
              meter_type:
                type: string
                default: llm_tokens
                description: >
                  Defaults to `llm_tokens`. Set to a platform meter type
                  (e.g. `compute_execution`) for a platform SKU.
              provider:
                type: string
              model:
                type: string
              component:
                type: string
                description: The component this row prices (`input_tokens`, `compute_second`, …)
              unit:
                type: string
                description: Unit `unit_price` is denominated in (`token`, `compute_second`, …)
              unit_price:
                type: number
                description: USD per `unit`
              effective_from:
                type: string
                format: date-time
                description: Must be in the future; past prices are immutable

    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          description: >-
            Structured error. Every error response uses this shape — 401, 403
            and the 500 catch-all included — so `code` can be read without
            first checking the type of `error`.
          required:
            - code
            - message
            - hint
            - docs_url
          properties:
            code:
              type: string
              description: A key from the server's ERROR_CODES registry.
              example: RESOURCE_NOT_FOUND
            message:
              type: string
              example: 'Resource not found'
            hint:
              type: string
              description: >-
                What to do about this error. Resolved per code, so a caller that
                has never seen the code before can act on the response without
                leaving it.
              example: >-
                Check the id, and check that the credential can see the project
                that owns the resource.
            docs_url:
              type: string
              format: uri
              description: The reference-page anchor documenting this code.
              example: >-
                https://soat.ttoss.dev/docs/error-codes#resource_not_found
            meta:
              type: object
              description: Optional structured context for the error.
