openapi: 3.1.0
info:
  title: fremai inference API
  version: "1.0.0"
  summary: EU-sovereign, OpenAI-compatible inference.
  description: >-
    fremai exposes an **OpenAI-compatible** inference API. Point any OpenAI SDK,
    `curl`, CI job, or opencode at the base URL below and switch models by name —
    no code changes beyond `baseURL` + your `sk-fremai-…` key.


    **Sovereignty:** requests are served on EU infrastructure (T Cloud Public,
    Germany). Prompts and completions are **never stored or used for training**;
    only billing-relevant token-usage metadata is retained (see the fremai DPA).


    **Compatibility:** the request/response shapes mirror the OpenAI API for the
    routes below. `/v1` is a compatibility marker, not a version axis — advertise
    and use `https://api.fremai.eu/v1`; the ingress also accepts the bare-root
    form for clients configured without `/v1`.
  contact:
    name: fremai
    url: https://www.fremai.eu
    email: hello@fremai.eu
  license:
    name: Proprietary
    url: https://docs.fremai.eu/

servers:
  - url: https://api.fremai.eu/v1
    description: Production (EU / T Cloud Public, Germany)

security:
  - bearerAuth: []

tags:
  - name: Chat
    description: Chat completions (the primary surface).
  - name: Completions
    description: Legacy text completions.
  - name: Embeddings
  - name: Models

paths:
  /chat/completions:
    post:
      tags: [Chat]
      operationId: createChatCompletion
      summary: Create a chat completion
      description: >-
        OpenAI-compatible. Set `stream: true` for a Server-Sent Events stream of
        `chat.completion.chunk` objects terminated by `data: [DONE]`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ChatCompletionRequest' }
      responses:
        '200':
          description: A chat completion (or an SSE stream when `stream:true`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ChatCompletionResponse' }
            text/event-stream:
              schema:
                type: string
                description: "SSE stream of `chat.completion.chunk` objects; ends with `data: [DONE]`."
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /completions:
    post:
      tags: [Completions]
      operationId: createCompletion
      summary: Create a text completion (legacy)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CompletionRequest' }
      responses:
        '200':
          description: A text completion.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CompletionResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /embeddings:
    post:
      tags: [Embeddings]
      operationId: createEmbedding
      summary: Create embeddings
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmbeddingRequest' }
      responses:
        '200':
          description: Embedding vectors.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EmbeddingResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '404': { $ref: '#/components/responses/ModelNotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /models:
    get:
      tags: [Models]
      operationId: listModels
      summary: List available models
      description: The curated fremai catalog available to your key. `id` is the value you pass as `model`.
      responses:
        '200':
          description: The model catalog.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelList' }
        '401': { $ref: '#/components/responses/Unauthorized' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        A fremai API key (`sk-fremai-…`) in the `Authorization: Bearer` header.
        Issue/rotate keys in the console at app.fremai.eu.

  responses:
    BadRequest:
      description: Malformed request.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Missing or invalid API key.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    PaymentRequired:
      description: Prepaid credit exhausted or budget cap reached. Top up in the console.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ModelNotFound:
      description: The requested `model` is not in your catalog.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Rate or concurrency limit exceeded. Retry after `Retry-After`.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait before retrying.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  schemas:
    ChatMessage:
      type: object
      required: [role, content]
      properties:
        role:
          type: string
          enum: [system, user, assistant, tool]
        content:
          type: string
        name: { type: string }
    ChatCompletionRequest:
      type: object
      required: [model, messages]
      properties:
        model:
          type: string
          description: A catalog model id (see `GET /models`), e.g. `glm-5.2`.
          examples: [glm-5.2]
        messages:
          type: array
          items: { $ref: '#/components/schemas/ChatMessage' }
        temperature: { type: number, minimum: 0, maximum: 2, default: 1 }
        top_p: { type: number, minimum: 0, maximum: 1 }
        max_tokens: { type: integer, minimum: 1 }
        stream: { type: boolean, default: false }
        stop:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
    ChatCompletionResponse:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: chat.completion }
        created: { type: integer }
        model: { type: string }
        choices:
          type: array
          items:
            type: object
            properties:
              index: { type: integer }
              message: { $ref: '#/components/schemas/ChatMessage' }
              finish_reason:
                type: string
                enum: [stop, length, tool_calls, content_filter]
        usage: { $ref: '#/components/schemas/Usage' }
    CompletionRequest:
      type: object
      required: [model, prompt]
      properties:
        model: { type: string }
        prompt:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
        max_tokens: { type: integer, minimum: 1 }
        temperature: { type: number, minimum: 0, maximum: 2 }
        stream: { type: boolean, default: false }
    CompletionResponse:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: text_completion }
        created: { type: integer }
        model: { type: string }
        choices:
          type: array
          items:
            type: object
            properties:
              index: { type: integer }
              text: { type: string }
              finish_reason: { type: string }
        usage: { $ref: '#/components/schemas/Usage' }
    EmbeddingRequest:
      type: object
      required: [model, input]
      properties:
        model: { type: string }
        input:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
        encoding_format: { type: string, enum: [float, base64], default: float }
    EmbeddingResponse:
      type: object
      properties:
        object: { type: string, const: list }
        model: { type: string }
        data:
          type: array
          items:
            type: object
            properties:
              object: { type: string, const: embedding }
              index: { type: integer }
              embedding: { type: array, items: { type: number } }
        usage: { $ref: '#/components/schemas/Usage' }
    Usage:
      type: object
      description: Backend-reported token counts (the basis for metering + billing).
      properties:
        prompt_tokens: { type: integer }
        completion_tokens: { type: integer }
        total_tokens: { type: integer }
    Model:
      type: object
      properties:
        id: { type: string, examples: [glm-5.2] }
        object: { type: string, const: model }
        created: { type: integer }
        owned_by: { type: string, const: fremai }
    ModelList:
      type: object
      properties:
        object: { type: string, const: list }
        data:
          type: array
          items: { $ref: '#/components/schemas/Model' }
    Error:
      type: object
      description: OpenAI-compatible error envelope.
      properties:
        error:
          type: object
          properties:
            message: { type: string }
            type: { type: string }
            param: { type: [string, "null"] }
            code: { type: [string, "null"] }
