openapi: "3.1.0"
info:
  title: Kasva
  version: "0.1.0"
  description: |
    Smart AI Routing. Public Kasva gateway. OpenAI-compatible chat,
    Anthropic Messages, Images, Videos, BYOK, accounts, usage, and credits.

    Authenticate with an issued `kv_…` key (`Authorization: Bearer`) or, on
    session routes, the `kv_session` cookie from the website. Download this
    file from `/openapi.yaml`. Human docs: `/docs.html`.
  contact:
    name: Kasva
    email: info@kasva.dev
    url: https://kasva.dev/docs.html
servers:
  - url: https://kasva.dev
    description: Production
  - url: http://127.0.0.1:8787
    description: Local wrangler
tags:
  - name: Meta
  - name: Auth
  - name: Keys
  - name: BYOK
  - name: Models
  - name: Chat
  - name: Images
  - name: Videos
  - name: Usage
  - name: Billing
  - name: MCP
security:
  - bearerAuth: []
paths:
  /health:
    get:
      operationId: getHealth
      tags: [Meta]
      summary: Liveness
      security: []
      responses:
        "200":
          description: Gateway is up
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
              example:
                status: ok
                environment: production
  /v1:
    get:
      operationId: getV1Root
      tags: [Meta]
      summary: Gateway index
      security: []
      responses:
        "200":
          description: Name, version, docs URL, public endpoint list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/V1Root"
  /v1/keys:
    post:
      operationId: postLegacyKey
      tags: [Keys]
      summary: Legacy issue key by email
      description: |
        One-shot email key without a password account. Prefer
        `POST /v1/auth/register`. IP limit 5/hour. Second issue for the same
        email is 409. Rotate with `POST /v1/auth/rotate-key`.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailRequest"
            example:
              email: you@example.com
      responses:
        "200":
          description: Key shown once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyIssued"
        "409":
          $ref: "#/components/responses/Error"
  /v1/auth/register:
    post:
      operationId: postRegister
      tags: [Auth]
      summary: Create account and gateway key
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
            example:
              email: you@example.com
              password: at-least-8-chars
              name: Optional
              accepted_terms: true
      responses:
        "200":
          description: Sets `kv_session`; `api_key` once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyIssued"
        "400":
          $ref: "#/components/responses/Error"
  /v1/auth/login:
    post:
      operationId: postLogin
      tags: [Auth]
      summary: Sign in
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
            example:
              email: you@example.com
              password: at-least-8-chars
      responses:
        "200":
          description: Sets `kv_session`; `api_key_masked` only
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LoginSession"
        "401":
          $ref: "#/components/responses/Error"
  /v1/auth/logout:
    post:
      operationId: postLogout
      tags: [Auth]
      summary: Sign out
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Session cleared
  /v1/auth/logout-all:
    post:
      operationId: postLogoutAll
      tags: [Auth]
      summary: Revoke other sessions
      description: Keeps the current cookie.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Other sessions revoked
  /v1/auth/sessions:
    get:
      operationId: getSessions
      tags: [Auth]
      summary: List active sessions
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Session ids and created_at
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        created_at:
                          type: string
                          format: date-time
  /v1/auth/me:
    get:
      operationId: getMe
      tags: [Auth]
      summary: Current user
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Email and masked key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
  /v1/auth/forgot-password:
    post:
      operationId: postForgotPassword
      tags: [Auth]
      summary: Request password reset
      description: Valid-shaped emails always get the same generic 200.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailRequest"
            example:
              email: you@example.com
      responses:
        "200":
          description: Generic success
  /v1/auth/reset-password:
    post:
      operationId: postResetPassword
      tags: [Auth]
      summary: Complete password reset
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token:
                  type: string
                password:
                  type: string
                  minLength: 8
            example:
              token: "…"
              password: new-password-8+
      responses:
        "200":
          description: Password updated
        "400":
          $ref: "#/components/responses/Error"
  /v1/auth/verify-email/request:
    post:
      operationId: postVerifyEmailRequest
      tags: [Auth]
      summary: Send email verification link
      description: API stays usable unverified. No-op when mail is unset.
      security: []
      responses:
        "200":
          description: Accepted
  /v1/auth/verify-email:
    post:
      operationId: postVerifyEmail
      tags: [Auth]
      summary: Confirm email
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
            example:
              token: "…"
      responses:
        "200":
          description: Email confirmed
  /v1/auth/rotate-key:
    post:
      operationId: postRotateKey
      tags: [Auth]
      summary: Rotate gateway key
      description: |
        Session cookie or Bearer. Returns a new `api_key` once; the previous
        key stops immediately. Stored BYOK moves to the new key. Limit 5/hour.
      security:
        - bearerAuth: []
        - sessionCookie: []
      responses:
        "200":
          description: New key once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyIssued"
  /v1/auth/claim-account:
    post:
      operationId: postClaimAccount
      tags: [Auth]
      summary: Claim a legacy key into an account
      description: Same `kv_…` key is kept. `accepted_terms` required.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClaimAccountRequest"
            example:
              email: you@example.com
              api_key: kv_…
              password: at-least-8-chars
              accepted_terms: true
      responses:
        "200":
          description: Account claimed
        "401":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
  /v1/auth/oauth/github:
    get:
      operationId: getOauthGithub
      tags: [Auth]
      summary: Start GitHub OAuth
      security: []
      responses:
        "302":
          description: Redirect to GitHub
        "503":
          $ref: "#/components/responses/Error"
    post:
      operationId: postOauthGithub
      tags: [Auth]
      summary: Start GitHub OAuth
      security: []
      responses:
        "302":
          description: Redirect to GitHub
        "503":
          $ref: "#/components/responses/Error"
  /v1/auth/oauth/google:
    get:
      operationId: getOauthGoogle
      tags: [Auth]
      summary: Start Google OAuth
      security: []
      responses:
        "302":
          description: Redirect to Google
        "503":
          $ref: "#/components/responses/Error"
    post:
      operationId: postOauthGoogle
      tags: [Auth]
      summary: Start Google OAuth
      security: []
      responses:
        "302":
          description: Redirect to Google
        "503":
          $ref: "#/components/responses/Error"
  /v1/auth/oauth/{provider}/callback:
    get:
      operationId: getOauthCallback
      tags: [Auth]
      summary: OAuth return
      security: []
      parameters:
        - $ref: "#/components/parameters/oauthProvider"
      responses:
        "302":
          description: Sets session cookie and redirects
  /v1/auth/keys:
    get:
      operationId: getAuthKeys
      tags: [Keys]
      summary: List named Kasva keys
      description: Mask only. Never returns the full `kv_…` secret.
      security:
        - bearerAuth: []
        - sessionCookie: []
      responses:
        "200":
          description: Active keys (masked)
    post:
      operationId: postAuthKeys
      tags: [Keys]
      summary: Create a named Kasva key
      description: |
        `name` required, 1–64 characters. Full `api_key` once. Duplicate
        active name → 409. Max 10 active keys → 400.
      security:
        - bearerAuth: []
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
            example:
              name: cursor
      responses:
        "200":
          description: Full key once
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyIssued"
        "400":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
  /v1/auth/keys/{id}:
    get:
      operationId: getAuthKey
      tags: [Keys]
      summary: One named key (mask only)
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/keyId"
      responses:
        "200":
          description: Masked key
        "404":
          $ref: "#/components/responses/Error"
    delete:
      operationId: deleteAuthKey
      tags: [Keys]
      summary: Revoke a named key
      description: Last remaining key → 400. Canonical rotate remains `POST /v1/auth/rotate-key`.
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - $ref: "#/components/parameters/keyId"
      responses:
        "200":
          description: Revoked
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/byok/keys:
    get:
      operationId: getByokKeys
      tags: [BYOK]
      summary: Which provider keys are stored
      description: Never returns secrets or last-4.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: configured vs enabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ByokKeys"
    post:
      operationId: postByokKeys
      tags: [BYOK]
      summary: Upsert or clear provider keys
      description: |
        Omit a field to leave it unchanged. Send `""` to delete that provider.
        `{ "enabled": { "openai": false } }` disables a stored key without
        resending it. Per-request provider headers are not accepted.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ByokKeysWrite"
            examples:
              save:
                value:
                  groq: gsk_...
                  openai: sk-...
              disable:
                value:
                  enabled:
                    openai: false
      responses:
        "200":
          description: Updated flags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ByokKeys"
  /v1/models:
    get:
      operationId: getModels
      tags: [Models]
      summary: Model list
      description: |
        OpenAI-compatible list. BYOK ids appear only for providers you have
        registered and not disabled. Hosted ids are `kasva/<catalog-id>`.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: OpenAI list object
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelList"
  /v1/chat/completions:
    post:
      operationId: postChatCompletions
      tags: [Chat]
      summary: Chat (OpenAI-compatible)
      description: |
        `stream: true` pipes OpenAI-compatible SSE. Response includes a
        `kasva` routing object. Image and video model ids are rejected
        here — use `POST /v1/images` or `POST /v1/videos`.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatRequest"
            example:
              model: kasva/auto
              messages:
                - role: user
                  content: Hello
              temperature: 0.7
              max_tokens: 1024
              stream: false
      responses:
        "200":
          description: Chat completion
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatResponse"
        "401":
          $ref: "#/components/responses/Error"
        "402":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"
  /v1/messages:
    post:
      operationId: postMessages
      tags: [Chat]
      summary: Chat (Anthropic Messages)
      description: |
        Anthropic-shaped request and response. Anthropic SDKs use base URL
        `https://kasva.dev`. The path is `/v1/messages`. Auth is Bearer
        and/or `x-api-key` with the Kasva `kv_…` key (not a provider `sk-ant-…`
        key). Bearer wins if both are sent. `anthropic-version` is optional.
        Required: `model`, `messages`, `max_tokens`.
      security:
        - bearerAuth: []
        - apiKeyHeader: []
      parameters:
        - name: anthropic-version
          in: header
          required: false
          schema:
            type: string
            example: "2023-06-01"
        - name: x-api-key
          in: header
          required: false
          schema:
            type: string
          description: Kasva `kv_…` key. Ignored when Bearer is present.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MessagesRequest"
            example:
              model: kasva/auto
              max_tokens: 1024
              messages:
                - role: user
                  content: Hello
      responses:
        "200":
          description: Anthropic Message
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MessagesResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
  /v1/images:
    post:
      operationId: postImages
      tags: [Images]
      summary: Generate or edit images
      description: |
        Hosted `kasva/image` and `kasva/image-pro` (operator, credits debit after
        success) plus BYOK generate. Prompt-only hosted generate (`n=1`). BYOK
        may add `input_references` to edit. Image models are not accepted on chat.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImagesRequest"
            examples:
              hosted:
                value:
                  model: kasva/image
                  prompt: A product photo of a ceramic mug
              generate:
                value:
                  model: byok/openai/gpt-image-2
                  prompt: A product photo of a ceramic mug
                  size: 1024x1024
              edit:
                value:
                  model: byok/openai/gpt-image-2
                  prompt: Keep the product; warm sunset background
                  input_references:
                    - type: image_url
                      image_url:
                        url: data:image/png;base64,....
      responses:
        "200":
          description: Upstream images JSON
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
  /v1/images/generations:
    post:
      operationId: postImagesGenerations
      tags: [Images]
      summary: Alias of POST /v1/images
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImagesRequest"
            example:
              model: byok/fal/flux-schnell
              prompt: A ceramic mug
      responses:
        "200":
          description: Upstream images JSON
  /v1/videos:
    post:
      operationId: postVideos
      tags: [Videos]
      summary: Create a video job
      description: |
        Hosted `kasva/video`. Returns `queued` with id prefix `vid_`. Poll
        `GET /v1/videos/{id}` then fetch bytes from
        `GET /v1/videos/{id}/content` when `completed`. Rate-limit applies to
        create only. Optional `seconds` is an integer 4–15 (default 5).
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VideosRequest"
            example:
              model: kasva/video
              prompt: A ceramic mug spinning on a table
              seconds: 5
      responses:
        "200":
          description: Queued video job
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VideoJob"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "402":
          $ref: "#/components/responses/Error"
        "503":
          $ref: "#/components/responses/Error"
  /v1/videos/{id}:
    get:
      operationId: getVideo
      tags: [Videos]
      summary: Poll a video job
      description: |
        Owner-only. Unknown or other-user ids are 404. Terminal jobs return
        stored status with no upstream poll. Queued and in-progress jobs poll
        once per GET.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: vid_ab12cd34
      responses:
        "200":
          description: Video job
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VideoJob"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/videos/{id}/content:
    get:
      operationId: getVideoContent
      tags: [Videos]
      summary: Download completed video bytes
      description: |
        MP4 bytes for the owner when status is `completed`. Otherwise 404.
        No JSON body.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: vid_ab12cd34
      responses:
        "200":
          description: MP4 bytes
          content:
            video/mp4:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
  /v1/usage:
    get:
      operationId: getUsage
      tags: [Usage]
      summary: Per-key usage stats
      security:
        - bearerAuth: []
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: [24h, 7d, 30d]
            default: 7d
      responses:
        "200":
          description: Totals, by_provider, by_model, by_family, daily, rate_limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Usage"
  /v1/credits:
    get:
      operationId: getCredits
      tags: [Billing]
      summary: Credit balance
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Ledger balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Credits"
  /v1/credits/history:
    get:
      operationId: getCreditsHistory
      tags: [Billing]
      summary: Credit pack grant history
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Latest credit_grants and Pro plan_events for the session user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditGrantList"
  /v1/credits/checkout:
    post:
      operationId: postCreditsCheckout
      tags: [Billing]
      summary: Credits pack checkout URL
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Checkout URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Checkout"
        "402":
          $ref: "#/components/responses/Error"
  /v1/billing/pro:
    post:
      operationId: postBillingPro
      tags: [Billing]
      summary: Pro $9/month checkout URL
      security:
        - sessionCookie: []
      responses:
        "200":
          description: Checkout URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Checkout"
        "402":
          $ref: "#/components/responses/Error"
  /v1/mcp/tools:
    get:
      operationId: getMcpTools
      tags: [MCP]
      summary: Tool catalog
      description: Catalog only; no remote execution.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Tool list
          content:
            application/json:
              schema:
                type: object
                properties:
                  tools:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        description:
                          type: string
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: lr
      description: Issued Kasva gateway key (`kv_…`).
    apiKeyHeader:
      type: apiKey
      in: header
      name: x-api-key
      description: Kasva `kv_…` gateway key (not a provider Anthropic key).
    sessionCookie:
      type: apiKey
      in: cookie
      name: kv_session
      description: Website session cookie. No secret in this document.
  parameters:
    keyId:
      name: id
      in: path
      required: true
      schema:
        type: string
    oauthProvider:
      name: provider
      in: path
      required: true
      schema:
        type: string
        enum: [github, google]
  responses:
    Error:
      description: Error envelope
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            message:
              type: string
            type:
              type: string
          example:
            message: Invalid or missing API key
            type: invalid_api_key
    Health:
      type: object
      properties:
        status:
          type: string
          example: ok
        environment:
          type: string
    V1Root:
      type: object
      properties:
        name:
          type: string
          example: Kasva
        version:
          type: string
          example: "0.1.0"
        docs:
          type: string
          example: https://kasva.dev/docs.html
        endpoints:
          type: array
          items:
            type: string
    EmailRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
    RegisterRequest:
      type: object
      required: [email, password, accepted_terms]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        name:
          type: string
        accepted_terms:
          type: boolean
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
    LoginSession:
      type: object
      required: [object, id, email, api_key_masked, created_at]
      properties:
        object:
          type: string
          example: user
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        api_key_masked:
          type: string
        created_at:
          type: string
          format: date-time
    ClaimAccountRequest:
      type: object
      required: [email, api_key, password, accepted_terms]
      properties:
        email:
          type: string
          format: email
        api_key:
          type: string
        password:
          type: string
          minLength: 8
        accepted_terms:
          type: boolean
    ApiKeyIssued:
      type: object
      properties:
        object:
          type: string
          example: api_key
        api_key:
          type: string
          example: kv_...
        email:
          type: string
        created_at:
          type: string
          format: date-time
    Me:
      type: object
      properties:
        email:
          type: string
        api_key_masked:
          type: string
    ByokKeys:
      type: object
      properties:
        object:
          type: string
          example: byok.keys
        configured:
          $ref: "#/components/schemas/ByokFlags"
        enabled:
          $ref: "#/components/schemas/ByokFlags"
    ByokFlags:
      type: object
      additionalProperties:
        type: boolean
      properties:
        groq:
          type: boolean
        deepseek:
          type: boolean
        gemini:
          type: boolean
        openai:
          type: boolean
        anthropic:
          type: boolean
        xai:
          type: boolean
        fal:
          type: boolean
    ByokKeysWrite:
      type: object
      properties:
        groq:
          type: string
        deepseek:
          type: string
        gemini:
          type: string
        openai:
          type: string
        anthropic:
          type: string
        xai:
          type: string
        fal:
          type: string
        enabled:
          $ref: "#/components/schemas/ByokFlags"
    ModelList:
      type: object
      properties:
        object:
          type: string
          example: list
        data:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              object:
                type: string
                example: model
              owned_by:
                type: string
    ChatRequest:
      type: object
      required: [model, messages]
      properties:
        model:
          type: string
          example: auto
        messages:
          type: array
          minItems: 1
          items:
            type: object
            required: [role, content]
            properties:
              role:
                type: string
              content:
                type: string
        temperature:
          type: number
        max_tokens:
          type: integer
        stream:
          type: boolean
    ChatResponse:
      type: object
      properties:
        id:
          type: string
        object:
          type: string
          example: chat.completion
        model:
          type: string
        choices:
          type: array
          items:
            type: object
        usage:
          type: object
          properties:
            prompt_tokens:
              type: integer
            completion_tokens:
              type: integer
            total_tokens:
              type: integer
        kasva:
          description: Kasva extension. Present on Kasva-routed completions.
          $ref: "#/components/schemas/Kasva"
        lumirouter:
          description: Legacy mirror of the `kasva` object (pre-rename clients). Same shape.
          $ref: "#/components/schemas/Kasva"
    Kasva:
      type: object
      properties:
        requested_model:
          type: string
        routed_model:
          type: string
        provider:
          type: string
        route_reason:
          type: string
    MessagesRequest:
      type: object
      required: [model, messages, max_tokens]
      properties:
        model:
          type: string
        max_tokens:
          type: integer
        messages:
          type: array
          minItems: 1
          items:
            type: object
            properties:
              role:
                type: string
                enum: [user, assistant]
              content:
                type: string
        stream:
          type: boolean
        temperature:
          type: number
        system:
          description: String or text-block array
        tools:
          type: array
          items:
            type: object
        tool_choice:
          description: auto, any, none, or a named tool
    MessagesResponse:
      type: object
      properties:
        type:
          type: string
          example: message
        id:
          type: string
        content:
          type: array
          items:
            type: object
        stop_reason:
          type: string
        usage:
          type: object
          properties:
            input_tokens:
              type: integer
            output_tokens:
              type: integer
    ImagesRequest:
      type: object
      required: [model, prompt]
      properties:
        model:
          type: string
          example: byok/openai/gpt-image-2
        prompt:
          type: string
        size:
          type: string
          example: 1024x1024
        n:
          type: integer
        input_references:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
                example: image_url
              image_url:
                type: object
                properties:
                  url:
                    type: string
    VideosRequest:
      type: object
      required: [model, prompt]
      properties:
        model:
          type: string
          example: kasva/video
        prompt:
          type: string
        seconds:
          type: integer
          minimum: 4
          maximum: 15
          default: 5
    VideoJob:
      type: object
      properties:
        object:
          type: string
          example: video
        id:
          type: string
          example: vid_ab12cd34
        model:
          type: string
          example: kasva/video
        status:
          type: string
          enum: [queued, in_progress, completed, failed]
        seconds:
          type: integer
        created_at:
          type: integer
        completed_at:
          type: integer
          nullable: true
        error:
          type: object
          nullable: true
          properties:
            message:
              type: string
    Usage:
      type: object
      properties:
        total_requests:
          type: integer
        total_estimated_cost_usd:
          type: number
        rate_limit:
          type: object
    Credits:
      type: object
      properties:
        balance_tokens:
          type: integer
          description: INTEGER small-units (1 unit = 1e-8 USD)
        balance_usd:
          type: number
        plan:
          type: string
        hosted_open:
          type: boolean
    CreditGrant:
      type: object
      properties:
        object:
          type: string
          enum: [credit_grant, plan_event]
        kind:
          type: string
          enum: [credits, pro]
        provider:
          type: string
        external_id:
          type: string
        tokens:
          type: integer
        amount_usd:
          type: number
        discount_usd:
          type: number
        plan:
          type: string
          enum: [pro, free]
        event_name:
          type: string
        created_at:
          type: integer
    CreditGrantList:
      type: object
      properties:
        object:
          type: string
          example: list
        data:
          type: array
          items:
            $ref: "#/components/schemas/CreditGrant"
    Checkout:
      type: object
      properties:
        object:
          type: string
          example: checkout
        url:
          type: string
          format: uri
