openapi: 3.0.3
info:
  title: SOAT AI Providers API
  version: 1.0.0
  description: API for managing LLM provider configurations (AI Providers resource)
  contact:
    name: SOAT Team
    url: https://github.com/ttoss/soat
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: AI Providers
    description: Manage AI providers
security:
  - bearerAuth: []
paths:
  /api/v1/ai-providers:
    get:
      tags:
        - AI Providers
      summary: List AI providers
      description: Returns a list of AI provider configurations for a project
      operationId: listAiProviders
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: limit
          in: query
          description: Number of results per page
          schema:
            type: integer
            default: 25
        - name: offset
          in: query
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of AI providers
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        provider:
                          type: string
                          enum: [openai, anthropic, google, xai, groq, ollama, azure, bedrock, vertex, gateway, custom]
                        default_model:
                          type: string
                        secret_id:
                          x-soat-ref: secrets
                          type: string
                          nullable: true
                          description: Secret ID containing API credentials, or null when the record links none.
                        base_url:
                          type: string
                          description: Custom base URL for the provider. Absent when the record sets none.
                        config:
                          type: object
                          description: Additional provider-specific configuration. Absent when the record sets none.
                        project_id:
                          x-soat-ref: projects
                          type: string
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - AI Providers
      summary: Create an AI provider
      description: Creates a new LLM provider configuration
      operationId: createAiProvider
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - provider
                - default_model
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                name:
                  type: string
                  description: Provider configuration name
                  example: OpenAI Production
                provider:
                  type: string
                  enum: [openai, anthropic, google, xai, groq, ollama, azure, bedrock, vertex, gateway, custom]
                  description: LLM provider
                  example: openai
                default_model:
                  type: string
                  description: Default model to use
                  example: gpt-4
                secret_id:
                  x-soat-ref: secrets
                  type: string
                  description: Secret ID containing API credentials
                  example: sec_V1StGXR8Z5jdHi6B
                base_url:
                  type: string
                  description: Custom base URL for the provider
                config:
                  type: object
                  description: Additional provider-specific configuration
      responses:
        '201':
          description: AI provider created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  provider:
                    type: string
                  default_model:
                    type: string
                  project_id:
                    x-soat-ref: projects
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '400':
          description: Bad request (invalid provider or missing fields)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/ai-providers/{ai_provider_id}:
    get:
      tags:
        - AI Providers
      summary: Get an AI provider
      description: Returns a specific AI provider configuration
      operationId: getAiProvider
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: AI provider details
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  provider:
                    type: string
                  default_model:
                    type: string
                  project_id:
                    x-soat-ref: projects
                    type: string
                  secret_id:
                    x-soat-ref: secrets
                    type: string
                    nullable: true
                  base_url:
                    type: string
                  config:
                    type: object
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
    patch:
      tags:
        - AI Providers
      summary: Update an AI provider
      description: Updates an AI provider configuration
      operationId: updateAiProvider
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                provider:
                  type: string
                  enum: [openai, anthropic, google, xai, groq, ollama, azure, bedrock, vertex, gateway, custom]
                  description: LLM provider
                  example: openai
                default_model:
                  type: string
                secret_id:
                  x-soat-ref: secrets
                  type: string
                  nullable: true
                base_url:
                  type: string
                config:
                  type: object
      responses:
        '200':
          description: AI provider updated successfully
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
    delete:
      tags:
        - AI Providers
      summary: Delete an AI provider
      description: >
        Deletes an AI provider configuration.


        Live references — chats, agents, and model routes whose
        targets name this provider — always block deletion with
        `409 AI_PROVIDER_HAS_DEPENDENTS`; `force` does
        not override them, so delete or repoint those resources first. Soft
        dependents — price overrides and usage/generation records — also block
        with `409` unless `force=true`, which deletes the provider's price
        overrides and unlinks (nulls) its usage history, preserving those rows.
        The `409` body's `error.meta`
        reports the counts, a sample of offending IDs, and a `forcible` flag that
        is `true` when a `force=true` retry would succeed.
      operationId: deleteAiProvider
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
        - name: force
          in: query
          required: false
          description: >
            When `true`, delete the provider's price overrides and unlink its
            usage history so a provider with only soft dependents can be
            removed. Has no effect on live references (chats, agents, model
            routes), which always block deletion.
          schema:
            type: boolean
            default: false
      responses:
        '204':
          description: AI provider deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
        '409':
          description: >
            The AI provider still has dependents. Live references always block;
            soft dependents block unless force=true. See error.meta for counts,
            offending IDs, and the forcible flag.
  /api/v1/ai-providers/{ai_provider_id}/models:
    get:
      tags:
        - AI Providers
      summary: List the models this provider can run
      description: >
        Asks the provider which models it can run, using this provider record's
        own credentials and configuration, and returns provider-native model ids
        — the same strings `default_model` and an agent's `model` carry.

        Which models are reachable is a property of the credential, not of the
        provider type: a Vertex provider sees only the publisher models its
        Google Cloud project and location serve, and a Bedrock provider only the
        foundation models enabled in its region. Reading the list is how a caller
        avoids pinning a model that fails at generation time.

        Not every provider type can answer. `azure` lists deployments an operator
        named rather than models, and `ollama` lists whatever was pulled onto that
        host, so both return `400 MODEL_LISTING_UNSUPPORTED`.

        Listing resolves credentials the same way generation does, so a record
        that can generate can list. The API-key types (`openai`, `groq`, `xai`,
        `gateway`, `custom`, `anthropic`, `google`) use the record's linked
        secret and cannot list without one. `bedrock` and `vertex` use the
        linked secret when there is one — IAM keys or a Bedrock API key, a
        Google service-account key — and otherwise fall back to the server
        environment (the AWS default credential chain, Google Application
        Default Credentials), so a record with no `secret_id` can still list.

        A Vertex record needs no `config.project` when its secret is a
        service-account key, since the key file names its own project. A Vertex
        record in express mode (API key) cannot list at all: the publisher-model
        listing rejects API keys and needs a credential that asserts a
        principal, so it returns `400 MODEL_LISTING_UNSUPPORTED`.

        The Vertex answer is the publisher catalogue the record's
        `config.location` region serves. The project behind the credential is
        billed and quota'd for the call but does not filter the result, so a
        listed model may still be unavailable to that project at generation
        time.
      operationId: listAiProviderModels
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: The models this provider can run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderModelsResponse'
        '400':
          description: >
            The provider type or authentication mode cannot enumerate models
            (including Vertex express mode), or the record is missing
            configuration the listing needs (a Vertex project from either
            `config.project` or the service-account key file, a Bedrock region,
            or — for the API-key provider types — a linked secret).
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
        '502':
          description: The provider rejected the listing request
  /api/v1/ai-providers/{ai_provider_id}/prices:
    get:
      tags:
        - AI Providers
      summary: List per-provider price overrides
      description: >
        Returns the per-provider price overrides for this AI provider instance.
        An override prices this specific provider (e.g. an enterprise-negotiated
        rate or a gateway with markup) and wins over the global default at cost
        time. Authorized by the caller's access to the provider's project — so,
        unlike the global price book, a project's own overrides are visible here.
      operationId: getAiProviderPrices
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: The provider's price overrides
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderPricesResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
    put:
      tags:
        - AI Providers
      summary: Upsert per-provider price overrides
      description: >
        Upserts price overrides for this AI provider instance, keyed on
        (model, effective_from). The provider slug is taken from the AI provider
        itself, so only the model, rates, and effective_from are supplied.
        Authorized by the caller's access to the provider's project.
        `effective_from` must be in the future once the (model, component) has a
        price row — past prices are immutable, so corrections ship as new
        future-dated rows. A first price for a (model, component) nothing prices
        yet may be dated now or earlier, so a new provider is never live and
        unpriced.
      operationId: updateAiProviderPrices
      parameters:
        - name: ai_provider_id
          in: path
          required: true
          description: AI Provider ID
          schema:
            type: string
            example: aip_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertProviderPricesRequest'
      responses:
        '200':
          description: The upserted price overrides
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderPricesResponse'
        '400':
          description: Bad Request (e.g. non-future effective_from)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: AI provider not found
components:
  schemas:
    ProviderPrice:
      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
          description: The AI provider instance this override prices
          example: aip_V1StGXR8Z5jdHi6B
        meter_type:
          type: string
          description: Always `llm_tokens` for provider price overrides
        provider:
          type: string
          description: Provider slug (taken from the AI provider instance)
          example: openai
        model:
          type: string
          example: gpt-4o
        component:
          type: string
          description: The token component this row prices (`input_tokens`, `output_tokens`, `cached_tokens`)
        unit:
          type: string
          description: Always `token` for provider price overrides
        unit_price:
          type: number
          description: USD per token for this component
        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

    ProviderPricesResponse:
      type: object
      properties:
        prices:
          type: array
          items:
            $ref: '#/components/schemas/ProviderPrice'

    UpsertProviderPricesRequest:
      type: object
      required:
        - prices
      properties:
        prices:
          type: array
          items:
            type: object
            required:
              - model
              - component
              - unit
              - unit_price
              - effective_from
            properties:
              model:
                type: string
                example: gpt-4o
              component:
                type: string
                description: The token component this row prices (`input_tokens`, `output_tokens`, `cached_tokens`)
              unit:
                type: string
                description: Always `token` for token pricing
              unit_price:
                type: number
                description: USD per token for this component
              effective_from:
                type: string
                format: date-time
                description: Must be in the future; past prices are immutable

    ProviderModelsResponse:
      type: object
      required:
        - provider
        - models
      properties:
        provider:
          type: string
          description: The provider slug the listing came from
          example: vertex
        models:
          type: array
          items:
            type: object
            required:
              - id
            properties:
              id:
                type: string
                description: >
                  The provider-native model id, ready to use as `default_model`
                  or an agent's `model`
                example: gemini-2.5-flash
              display_name:
                type: string
                description: The provider's own human-readable name, when it reports one
                example: Gemini 2.5 Flash
              vendor:
                type: string
                description: Who makes the model, when the provider reports it
                example: google
              input_modalities:
                type: array
                description: Lowercased input modalities, when the provider reports them
                items:
                  type: string
                example:
                  - text
                  - image
              output_modalities:
                type: array
                description: Lowercased output modalities, when the provider reports them
                items:
                  type: string
                example:
                  - text
              streaming:
                type: boolean
                description: Whether the model supports streaming responses
              lifecycle:
                type: string
                description: >
                  `active`, `legacy` or `deprecated`, as the provider reports it.
                  A model that is not `active` still serves today but should not
                  be pinned by anything new.
                enum:
                  - active
                  - legacy
                  - deprecated
              inference_types:
                type: array
                description: >
                  Lowercased inference types the model supports, when reported.
                  A Bedrock model offering only `inference_profile` must be
                  invoked through a cross-region profile id rather than the bare
                  model id.
                items:
                  type: string
                example:
                  - on_demand
                  - inference_profile

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT token or sk_ api key
