# SPDX-License-Identifier: LicenseRef-Whale-Proprietary
# Copyright (c) 2026 Martin Schröder <info@swedishembedded.com>
#
# Hand-maintained: what the paid API serves is exactly the routes below.
# When the gateway gains a route, this file gains it in the same change.
openapi: 3.0.3
info:
  title: sven API
  version: "1"
  description: |
    Run chat, embedding and image models over HTTP, and search the web. The
    API speaks the OpenAI dialect, so an existing OpenAI client works by
    changing its base URL and key; /v1/messages speaks Anthropic's, for
    Anthropic's client. /v1/search is a web search, sold per search.

    Every call needs an API key and is billed for what it delivered, at the
    prices on the pricing page. Before a call runs, the most it could cost is
    set aside from the account's credit; a call that credit, the key's
    spending limit or the workspace's monthly budget cannot cover is refused
    with 402 before anything runs.

    Send an Idempotency-Key header to make a retry safe: the same key answers
    the first call's stored result instead of running and charging again.
servers:
  - url: https://api.example.invalid
    description: Replace with the host your account is served from.
security:
  - bearer: []
  - apiKey: []
paths:
  /v1/models:
    get:
      operationId: listModels
      summary: The models this key may call. A model with no price is absent; it is not for sale.
      responses:
        "200":
          description: |
            The models, in the OpenAI list shape, each entry also being an
            OpenRouter model object, so a client written against OpenRouter's
            /models reads it unchanged: `name`, `description`,
            `context_length`, `architecture`, `top_provider`,
            `supported_parameters` (as the model's upstream listed them; null
            where nobody did), `pricing` (what it is sold for, in exact US
            dollars per token for `prompt` and `completion`, per image for
            `image`, "0" for fees not charged) and `capabilities` (chat,
            tools, embeddings, images).
        "401": { $ref: "#/components/responses/Error" }
  /v1/chat/completions:
    post:
      operationId: createChatCompletion
      summary: Chat completion, optionally streamed as server-sent events.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { $ref: "#/components/requestBodies/ChatCompletionRequest" }
      responses:
        "200": { description: The completion (`application/json`) or an event stream (`text/event-stream`), in the OpenAI shapes. }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "413": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "500": { $ref: "#/components/responses/Error" }
        "503": { $ref: "#/components/responses/Error" }
  /v1/embeddings:
    post:
      operationId: createEmbedding
      summary: Embeddings of one or more texts, as float arrays.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, input]
              properties:
                model: { type: string, description: A model id from GET /v1/models. }
                input:
                  description: One text, or up to 2048 texts.
                  oneOf:
                    - { type: string, minLength: 1 }
                    - { type: array, minItems: 1, maxItems: 2048, items: { type: string, minLength: 1 } }
                encoding_format: { type: string, enum: [float] }
      responses:
        "200": { description: The embeddings, in the OpenAI list shape, with `usage.prompt_tokens`. }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "500": { $ref: "#/components/responses/Error" }
        "503": { $ref: "#/components/responses/Error" }
  /v1/search:
    post:
      operationId: searchWeb
      summary: Web search results for a query, charged once per search.
      description: |
        One search is one charge, at the per-search price on the pricing
        page; a search that fails is not charged. sven's web_search tool
        calls this route when it is signed in to this service.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query: { type: string, minLength: 1, maxLength: 400, description: What to search for. }
                count: { type: integer, minimum: 1, maximum: 20, default: 5, description: How many results. }
      responses:
        "200":
          description: The results, best first.
          content:
            application/json:
              schema:
                type: object
                required: [object, query, results]
                properties:
                  object: { type: string, enum: [search.results] }
                  query: { type: string }
                  results:
                    type: array
                    items:
                      type: object
                      required: [title, url, description]
                      properties:
                        title: { type: string }
                        url: { type: string }
                        description: { type: string }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "500": { $ref: "#/components/responses/Error" }
        "503": { $ref: "#/components/responses/Error" }
  /v1/images/generations:
    post:
      operationId: createImage
      summary: Images from a prompt, returned inline as base64 PNG.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, prompt]
              properties:
                model: { type: string, description: A model id from GET /v1/models. }
                prompt: { type: string, minLength: 1 }
                n: { type: integer, minimum: 1, maximum: 10, default: 1 }
                size: { type: string, pattern: "^[0-9]+x[0-9]+$", description: "WIDTHxHEIGHT, each side 1 to 4096." }
                response_format: { type: string, enum: [b64_json] }
      responses:
        "200": { description: "`{created, data: [{b64_json}]}`; charged per image delivered." }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "500": { $ref: "#/components/responses/Error" }
        "503": { $ref: "#/components/responses/Error" }
  /v1/messages:
    post:
      operationId: createMessage
      summary: Anthropic Messages, optionally streamed; for Anthropic's client.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, messages, max_tokens]
              properties:
                model: { type: string }
                messages: { type: array, items: { type: object } }
                max_tokens: { type: integer, minimum: 1 }
                stream: { type: boolean }
      responses:
        "200": { description: The message or an event stream, in Anthropic's shapes. }
        "400": { description: An Anthropic-shaped error object. }
        "401": { description: An Anthropic-shaped error object. }
        "402": { description: An Anthropic-shaped error object. }
        "429": { description: An Anthropic-shaped error object. }
components:
  securitySchemes:
    bearer: { type: http, scheme: bearer, description: "`Authorization: Bearer whale_...`, as OpenAI's clients send it." }
    apiKey: { type: apiKey, in: header, name: x-api-key, description: "The same key, as Anthropic's clients send it." }
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: 1 to 255 printable characters. A retry with the same value answers the first call's result without a second charge.
      schema: { type: string, minLength: 1, maxLength: 255 }
  requestBodies:
    ChatCompletionRequest:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [model, messages]
            properties:
              model: { type: string, description: A model id from GET /v1/models. }
              messages:
                type: array
                items: { type: object }
              stream: { type: boolean, description: Server-sent events instead of one JSON body. }
              stream_options:
                type: object
                properties:
                  include_usage: { type: boolean }
  responses:
    Error:
      description: |
        An OpenAI-shaped error object. 400 a malformed request or a parameter
        the model refuses (`error.param` names it); 401 no, unknown, expired
        or revoked key; 402 the account's credit, the key's spending limit or
        the workspace's monthly budget cannot cover the call (`error.code`
        says which); 403 the key may not use this model; 404 unknown or
        unpriced model; 409 a call with this Idempotency-Key is still running,
        or its stored answer has expired; 413 body past the ceiling; 429 a
        rate or concurrency limit, or the model is busy (`Retry-After` says
        when); 500 something failed inside the service; 503 the model is
        temporarily unavailable.
      content: { application/json: { schema: { type: object } } }
