openapi: 3.0.3
info:
  title: Chats API
  version: 1.0.0
  description: >
    Chat resources and completions. Create stateful chat objects bound to an AI
    provider, then run completions against them. Also exposes a stateless
    completions endpoint compatible with the OpenAI Chat Completions API.
  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: Chats
    description: Manage chats

security:
  - bearerAuth: []

paths:
  /api/v1/chats:
    post:
      tags:
        - Chats
      summary: Create a chat
      description: Creates a new chat resource bound to an AI provider.
      operationId: createChat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateChatRequest'
            examples:
              basic:
                summary: Minimal chat
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
              with_instructions:
                summary: Chat with instructions
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
                  name: Support Bot
                  instructions: You are a helpful support assistant.
                  model: gpt-4o
      responses:
        '201':
          description: Chat created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '400':
          description: Bad Request
          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'
        '404':
          description: AI provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    get:
      tags:
        - Chats
      summary: List chats
      description: Returns all chats in the project.
      operationId: listChats
      parameters:
        - name: project_id
          in: query
          required: false
          schema:
            type: string
          description: Project public ID to filter by
        - 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: List of chats
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Chat'
                  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/chats/{chat_id}:
    get:
      tags:
        - Chats
      summary: Get a chat
      description: Returns a single chat by ID.
      operationId: getChat
      parameters:
        - name: chat_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Chat record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chat'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Chat not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Chats
      summary: Delete a chat
      description: Deletes a chat by ID.
      operationId: deleteChat
      parameters:
        - name: chat_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Chat deleted
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Chat not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/chat/completions:
    post:
      tags:
        - Chats
      summary: Create a chat completion
      description: >
        OpenAI Chat Completions-compatible endpoint. Mirrors OpenAI's
        `POST /v1/chat/completions` path so an OpenAI SDK can target it by base
        URL alone.


        Names exactly one target. With `ai_provider_id` the completion is
        stateless: the provider's secret is decrypted and the appropriate Vercel
        AI SDK provider is called, with no server-side model fallback. With
        `chat_id` the stored chat supplies the provider (or the project's
        `default_model_route_id`), model and instructions.


        System content travels only in `instructions` — a `role: "system"` entry
        in `messages` is refused with `400 SYSTEM_MESSAGE_NOT_ALLOWED`. With
        `chat_id`, a request `instructions` replaces the chat's stored one for
        this call only; the stored value applies when the request carries none,
        and the two are never merged.


        Messages may use `document_id` instead of `content` with either target.
        Chats hold no message history — send the full `messages` array every
        time.
      operationId: createChatCompletion
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatCompletionRequest'
            examples:
              basic:
                summary: Simple user message
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
                  messages:
                    - role: user
                      content: Hello, how are you?
              with_chat:
                summary: Against a stored chat
                value:
                  chat_id: chat_V1StGXR8Z5jdHi6B
                  messages:
                    - role: user
                      content: What can you help me with?
              with_document:
                summary: Message referencing a document
                value:
                  chat_id: chat_V1StGXR8Z5jdHi6B
                  messages:
                    - role: user
                      document_id: doc_V1StGXR8Z5jdHi6B
              with_provider:
                summary: With explicit AI provider and streaming
                value:
                  ai_provider_id: aip_V1StGXR8Z5jdHi6B
                  model: gpt-4o
                  instructions: You are a helpful assistant.
                  messages:
                    - role: user
                      content: What files do I have?
                  stream: true
      responses:
        '200':
          description: Chat completion result (JSON or SSE stream)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatCompletionResponse'
            text/event-stream:
              schema:
                type: string
                description: >
                  SSE stream of JSON objects, one per line, prefixed with
                  `data: `. The stream ends with `data: [DONE]`.

                  The response headers are written before the provider is
                  called, so a failure cannot become a status code once the
                  stream is open. It arrives instead as a terminal
                  `data: {"error": "..."}` frame carrying the same mapped
                  message the non-streaming path returns in its `502` body
                  (e.g. `Provider returned 404: ...`), and the stream then
                  ends without a `[DONE]`.
        '400':
          description: >-
            Bad Request — `messages` is missing or empty; a `role: "system"`
            entry appears in `messages`; neither `ai_provider_id` nor `chat_id`
            was given, or both were; or `chat_id` names a chat that does not
            exist.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: >-
            Forbidden — the caller lacks `chats:CreateChatCompletion` on the
            project the call belongs to: the chat's project for `chat_id`, the
            AI provider's project for `ai_provider_id`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: AI provider not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: >
            Upstream AI provider error (AI_PROVIDER_ERROR) — the provider
            rejected the completion (an unavailable model, a refused
            credential) or could not be reached. The message names the
            provider's own status and message, so an unavailable model is
            distinguishable from a fault in the runtime itself. Streaming
            requests report this in a terminal SSE frame instead, since their
            status line is already on the wire.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT token or sk_ api key
  schemas:
    Chat:
      type: object
      properties:
        id:
          type: string
          description: Public ID of the chat
          example: chat_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the owning project
          example: proj_V1StGXR8Z5jdHi6B
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          nullable: true
          description: >-
            Public ID of the pinned AI provider, or `null` when the chat pins
            none and inherits its project's `default_model_route_id`.
          example: aip_V1StGXR8Z5jdHi6B
        name:
          type: string
          nullable: true
          description: Optional human-readable name
          example: Support Bot
        instructions:
          type: string
          nullable: true
          description: Optional system message sent with every completion
          example: You are a helpful support assistant.
        model:
          type: string
          nullable: true
          description: Optional model override for this chat
          example: gpt-4o
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CreateChatRequest:
      type: object
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          description: >-
            Public ID of the AI provider to pin. Optional: omit it to inherit the
            project's `default_model_route_id`, which gives the chat provider
            failover. Omitting it returns `400` when the project has no default,
            and cannot be combined with `model` — each route target names its own.
          example: aip_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
          description: >
            Public ID of the project. Required when the user belongs to multiple
            projects and no project key is used.
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          description: Optional human-readable name
          example: Support Bot
        instructions:
          type: string
          description: Optional system message applied to all completions on this chat
          example: You are a helpful support assistant.
        model:
          type: string
          description: Optional default model override
          example: gpt-4o

    ChatMessageInput:
      type: object
      required:
        - role
      properties:
        role:
          type: string
          # No `system`: system content travels only in `instructions`; a
          # system entry here is refused with 400 SYSTEM_MESSAGE_NOT_ALLOWED.
          enum:
            - user
            - assistant
          example: user
        content:
          type: string
          description: Text content of the message (mutually exclusive with documentId)
          example: What can you help me with?
        document_id:
          x-soat-ref: documents
          type: string
          description: >
            Public ID of a document whose content is used as the message body
            (mutually exclusive with content). Only valid for user/assistant roles.
          example: doc_V1StGXR8Z5jdHi6B

    ChatCompletionRequest:
      type: object
      required:
        - messages
      properties:
        ai_provider_id:
          x-soat-ref: ai-providers
          type: string
          description: >
            Public ID of the AI provider to run the completion against.
            Mutually exclusive with `chat_id`; exactly one of the two is
            required.
          example: aip_V1StGXR8Z5jdHi6B
        chat_id:
          x-soat-ref: chats
          type: string
          description: >
            Public ID of a stored chat supplying the provider, model and
            instructions. Mutually exclusive with `ai_provider_id`; exactly one
            of the two is required.
          example: chat_V1StGXR8Z5jdHi6B
        model:
          type: string
          description: >
            Model identifier. Overrides the provider's `default_model`, or the
            chat's `model`, when specified.
          example: gpt-4o
        instructions:
          type: string
          description: >
            System instructions for this call. Sent to the provider as its
            `instructions` argument rather than as a message, which is the only
            place the AI SDK accepts system content (`allowSystemInMessages`
            defaults to false). This field is the only channel — a
            `role: "system"` entry in `messages` is refused with
            `400 SYSTEM_MESSAGE_NOT_ALLOWED`. With `chat_id`, this replaces the
            chat's stored `instructions` for this call only; the stored value
            applies when the request carries none, and the two are never merged.
          example: You are a helpful assistant.
        messages:
          type: array
          minItems: 1
          description: Ordered list of chat messages
          items:
            $ref: '#/components/schemas/ChatMessageInput'
        stream:
          type: boolean
          default: false
          description: >
            When `true` the response is an SSE stream of delta chunks.
            When `false` (default) a single JSON object is returned.

    ChatCompletionResponseMessage:
      type: object
      properties:
        role:
          type: string
          example: assistant
        content:
          type: string
          example: Hello! I am doing well, thank you.

    ChatCompletionChoice:
      type: object
      properties:
        index:
          type: integer
          example: 0
        message:
          $ref: '#/components/schemas/ChatCompletionResponseMessage'
        finish_reason:
          type: string
          example: stop

    ChatCompletionResponse:
      type: object
      properties:
        object:
          type: string
          example: chat.completion
        model:
          type: string
          example: gpt-4o
        choices:
          type: array
          items:
            $ref: '#/components/schemas/ChatCompletionChoice'

    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.
