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 and generation. 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: 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: 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/UsageMeter'
                  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/receipt:
    get:
      tags:
        - Usage
      summary: Get a generation's billing receipt
      description: >
        Returns a billing receipt for a completed generation: per-model line
        items (tokens, the price-book version that priced them, and cost) plus
        totals.
      operationId: getUsageReceipt
      parameters:
        - name: generation_id
          in: query
          required: true
          description: Generation public ID
          schema:
            type: string
      responses:
        '200':
          description: The generation's 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 — past prices are immutable
        so recorded costs stay explainable; ship corrections as new
        future-dated rows.
      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:
    UsageMeter:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the usage-meter row
          example: um_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project the usage is attributed to
        run_id:
          type: string
          nullable: true
          description: >
            Public ID of the orchestration run that initiated the call, when
            the generation ran inside a run. Null for standalone generations.
        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
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >
            Public ID of the AI provider instance billed. Correlates the meter
            to the price book. 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.
        provider:
          type: string
          description: >
            Denormalized as-billed provider slug, retained even if the AI
            provider is later deleted
          example: openai
        model:
          type: string
          description: Model identifier the provider billed
          example: gpt-4o
        input_tokens:
          type: integer
          description: Input (prompt) tokens reported by the provider
        output_tokens:
          type: integer
          description: Output (completion) tokens reported by the provider
        cached_tokens:
          type: integer
          description: Cached input tokens read, when the provider reports them
        reasoning_tokens:
          type: integer
          description: >
            Reasoning tokens the provider reports separately (e.g. OpenAI
            `completion_tokens_details.reasoning_tokens`). 0 when unreported.
        cost_usd:
          type: number
          nullable: true
          description: >
            Cost in USD computed at write time from the versioned price book.
            Null when the tokens are captured but not yet priced.
        price_id:
          type: string
          nullable: true
          description: >
            Public ID of the price-book row that produced `cost_usd` (the
            price-table version). Null when no price applied.
        created_at:
          type: string
          format: date-time

    UsageReceipt:
      type: object
      properties:
        generation_id:
          x-soat-ref: generations
          type: string
        currency:
          type: string
          example: USD
        line_items:
          type: array
          items:
            type: object
            properties:
              provider:
                type: string
              model:
                type: string
              price_id:
                type: string
                nullable: true
                description: Price-book version that priced this line
              input_tokens:
                type: integer
              output_tokens:
                type: integer
              cached_tokens:
                type: integer
              reasoning_tokens:
                type: integer
              cost_usd:
                type: number
                nullable: true
        total_input_tokens:
          type: integer
        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 line costs; null when no line is priced

    Price:
      type: object
      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.
        provider:
          type: string
          example: openai
        model:
          type: string
          example: gpt-4o
        input_price_per_m:
          type: number
          description: USD per one million input (prompt) tokens
        output_price_per_m:
          type: number
          description: USD per one million output (completion) tokens
        cached_price_per_m:
          type: number
          nullable: true
          description: USD per one million cached input tokens; null falls back to the input price
        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
              - input_price_per_m
              - output_price_per_m
              - effective_from
            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.
              provider:
                type: string
              model:
                type: string
              input_price_per_m:
                type: number
              output_price_per_m:
                type: number
              cached_price_per_m:
                type: number
                nullable: true
              effective_from:
                type: string
                format: date-time
                description: Must be in the future; past prices are immutable

    ErrorResponse:
      type: object
      properties:
        error:
          oneOf:
            - type: string
              description: Generic error message (e.g. "Internal Server Error")
            - type: object
              description: Structured domain error
              properties:
                code:
                  type: string
                  example: RESOURCE_NOT_FOUND
                message:
                  type: string
                meta:
                  type: object
