openapi: 3.1.0
info:
  title: KeyDealer API
  version: 0.3.0
  description: |
    OpenAI-compatible access to the public KeyDealer model catalog.
    Unknown top-level request parameters are rejected explicitly; per-model capabilities
    and billing rules are machine-readable before a paid request is sent.
servers:
  - url: https://api.keydealer.ru/v1
security:
  - bearerAuth: []
paths:
  /:
    get:
      security: []
      operationId: getServiceContract
      summary: Get service endpoints, request contract and current technical limits
      responses:
        '200':
          description: Machine-readable KeyDealer service contract.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /models:
    get:
      security: []
      operationId: listModels
      summary: List currently available models, base rates and verified capabilities
      responses:
        '200':
          description: Public model catalog. Capability status is verified, unsupported, or unknown.
          content:
            application/json:
              schema:
                type: object
                required: [object, data]
                properties:
                  object:
                    const: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Model'
  /balance:
    get:
      operationId: getBalance
      summary: Get exact account balance, effective rates and token equivalents
      responses:
        '200':
          description: Current balance and account-effective prices for the API key owner. Never cached.
          headers:
            Cache-Control:
              schema:
                type: string
              description: Always no-store for this endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Balance'
        '401':
          $ref: '#/components/responses/ApiError'
  /chat/completions:
    post:
      operationId: createChatCompletion
      summary: Create an OpenAI-compatible chat completion
      description: |
        The selected model's capabilities are published by GET /models. A request for a
        capability marked unsupported fails before execution. A request for a capability
        marked unknown may be attempted and is disclosed in x-kd-unverified-capabilities.
        Unknown top-level request fields fail with error.code=unsupported_parameter.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatCompletionRequest'
      responses:
        '200':
          description: Completion JSON or an SSE stream when stream is true.
          headers:
            x-request-id:
              schema:
                type: string
              description: Stable operation identifier also returned in keydealer_billing.operation_id.
            x-kd-route-context-tokens:
              schema:
                oneOf:
                  - type: integer
                  - const: unknown
              description: Pre-request estimate for route context; the measured usage is authoritative.
            x-kd-unverified-capabilities:
              schema:
                type: string
              description: Comma-separated requested capabilities whose status is unknown for this model.
            X-RateLimit-Limit:
              schema:
                type: integer
            X-RateLimit-Remaining:
              schema:
                type: integer
            Retry-After:
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
            text/event-stream:
              schema:
                type: string
        '400':
          $ref: '#/components/responses/ApiError'
        '401':
          $ref: '#/components/responses/ApiError'
        '402':
          $ref: '#/components/responses/ApiError'
        '404':
          $ref: '#/components/responses/ApiError'
        '413':
          $ref: '#/components/responses/ApiError'
        '429':
          $ref: '#/components/responses/ApiError'
        '502':
          $ref: '#/components/responses/ApiError'
        '503':
          $ref: '#/components/responses/ApiError'
  /images/generations:
    post:
      operationId: createImage
      summary: Generate one image with GPT Image
      description: |
        Generates exactly one image and returns base64 only. Billing is a fixed published
        price per image determined by model, size and quality. The account-effective price
        (including an individual multiplier) is available from GET /balance before generation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageGenerationRequest'
      responses:
        '200':
          description: Generated base64 image and exact fixed-price billing receipt.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImageGenerationResponse'
        '400': { $ref: '#/components/responses/ApiError' }
        '401': { $ref: '#/components/responses/ApiError' }
        '402': { $ref: '#/components/responses/ApiError' }
        '404': { $ref: '#/components/responses/ApiError' }
        '429': { $ref: '#/components/responses/ApiError' }
        '502': { $ref: '#/components/responses/ApiError' }
        '503': { $ref: '#/components/responses/ApiError' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: kd_live
  schemas:
    CapabilityStatus:
      type: string
      enum: [verified, unsupported, unknown]
      description: verified is probe-confirmed, unsupported is known not to work, unknown is not yet claimed.
    Capabilities:
      type: object
      additionalProperties: false
      required:
        - chat_completions
        - streaming
        - tools
        - forced_tool_choice
        - parallel_tool_calls
        - tool_result_messages
        - streaming_with_tools
        - json_output
        - max_output_tokens
      properties:
        chat_completions: { $ref: '#/components/schemas/CapabilityStatus' }
        streaming: { $ref: '#/components/schemas/CapabilityStatus' }
        tools: { $ref: '#/components/schemas/CapabilityStatus' }
        forced_tool_choice: { $ref: '#/components/schemas/CapabilityStatus' }
        parallel_tool_calls: { $ref: '#/components/schemas/CapabilityStatus' }
        tool_result_messages: { $ref: '#/components/schemas/CapabilityStatus' }
        streaming_with_tools: { $ref: '#/components/schemas/CapabilityStatus' }
        json_output: { $ref: '#/components/schemas/CapabilityStatus' }
        max_output_tokens:
          type: [integer, 'null']
          minimum: 1
          maximum: 65536
    Model:
      oneOf:
        - $ref: '#/components/schemas/ChatModel'
        - $ref: '#/components/schemas/ImageModel'
    ChatModel:
      type: object
      required: [id, object, owned_by, pricing, billing, capabilities, prompt_cache]
      properties:
        id: { type: string }
        object: { const: model }
        created: { type: integer }
        owned_by: { const: keydealer }
        name: { type: string }
        family: { type: string }
        purpose: { type: string }
        pricing:
          type: object
          required: [currency, scope, input_rub_per_million, output_rub_per_million, effective_pricing_endpoint]
          properties:
            currency: { const: RUB }
            scope: { const: base_catalog }
            input_rub_per_million: { type: number, minimum: 0 }
            output_rub_per_million: { type: number, minimum: 0 }
            effective_pricing_endpoint: { const: 'GET /v1/balance' }
        billing:
          type: object
          required: [source, formula, route_context]
          properties:
            source: { const: usage }
            formula: { type: string }
            estimated_route_context_tokens: { type: [integer, 'null'], minimum: 0 }
            context_value_is_estimate: { type: boolean }
            context_measured_at: { type: [string, 'null'], format: date-time }
            route_context:
              type: object
              required: [estimated_tokens, p50_tokens, p95_tokens, sample_size, is_estimate, may_vary_by_request]
              properties:
                estimated_tokens: { type: [integer, 'null'], minimum: 0 }
                p50_tokens: { type: [integer, 'null'], minimum: 0 }
                p95_tokens: { type: [integer, 'null'], minimum: 0 }
                sample_size: { type: [integer, 'null'], minimum: 1 }
                measured_at: { type: [string, 'null'], format: date-time }
                is_estimate: { const: true }
                may_vary_by_request: { const: true }
        capabilities:
          $ref: '#/components/schemas/Capabilities'
        capabilities_verified_at:
          type: [string, 'null']
          format: date-time
        prompt_cache:
          type: object
          required: [status, disclosure]
          properties:
            status:
              type: string
              enum: [verified, unsupported, unknown]
            disclosure: { type: string }
    ImageModel:
      type: object
      required: [id, object, owned_by, endpoint, pricing, billing, capabilities]
      properties:
        id: { type: string, enum: [gpt-image-2, gpt-image-1.5] }
        object: { const: model }
        created: { type: integer }
        owned_by: { const: keydealer }
        name: { type: string }
        family: { const: GPT Image }
        purpose: { type: string }
        endpoint: { const: 'POST /v1/images/generations' }
        pricing:
          type: object
          required: [currency, scope, unit, formula, effective_pricing_endpoint, by_size_and_quality]
          properties:
            currency: { const: RUB }
            scope: { const: base_catalog }
            unit: { const: image }
            formula: { const: fixed_price_per_image }
            effective_pricing_endpoint: { const: 'GET /v1/balance' }
            by_size_and_quality:
              type: object
              additionalProperties:
                type: object
                required: [low, medium, high]
                properties:
                  low: { type: number, minimum: 0 }
                  medium: { type: number, minimum: 0 }
                  high: { type: number, minimum: 0 }
        billing:
          type: object
          required: [source, formula, usage_tokens_required]
          properties:
            source: { const: fixed_price }
            formula: { type: string }
            usage_tokens_required: { const: false }
            policy_url: { type: string, format: uri }
        capabilities:
          type: object
          required: [image_generation, image_edits, n_max, sizes, qualities, output_formats]
          properties:
            image_generation: { const: verified }
            image_edits: { const: unsupported }
            n_max: { const: 1 }
            sizes: { type: array, items: { type: string } }
            qualities: { type: array, items: { type: string } }
            output_formats: { type: array, items: { type: string } }
    Balance:
      type: object
      required:
        - object
        - currency
        - pricing_scope
        - balance_microrubles
        - balance_rub
        - price_multiplier_bps
        - token_equivalents
        - image_equivalents
      properties:
        object: { const: balance }
        currency: { const: RUB }
        pricing_scope: { const: account_effective }
        balance_microrubles:
          type: string
          pattern: '^-?[0-9]+$'
          description: Exact balance in millionths of a ruble.
        balance_rub: { type: string }
        price_multiplier_bps: { type: integer, minimum: 1 }
        token_equivalents:
          type: array
          items:
            type: object
            required: [model, pricing_scope, input_tokens, output_tokens, input_rub_per_million, output_rub_per_million]
            properties:
              model: { type: string }
              pricing_scope: { const: account_effective }
              input_tokens: { type: string, pattern: '^[0-9]+$' }
              output_tokens: { type: string, pattern: '^[0-9]+$' }
              input_rub_per_million: { type: number, minimum: 0 }
              output_rub_per_million: { type: number, minimum: 0 }
        image_equivalents:
          type: array
          items:
            type: object
            required: [model, pricing_scope, prices]
            properties:
              model: { type: string }
              pricing_scope: { const: account_effective }
              prices:
                type: object
                additionalProperties:
                  type: object
                  additionalProperties:
                    type: object
                    required: [images, price_microrubles, price_rub]
                    properties:
                      images: { type: string, pattern: '^[0-9]+$' }
                      price_microrubles: { type: string, pattern: '^[0-9]+$' }
                      price_rub: { type: string }
    ChatMessage:
      type: object
      required: [role]
      additionalProperties: true
      properties:
        role:
          type: string
          enum: [system, developer, user, assistant, tool, function]
        content: {}
        name: { type: string }
        tool_call_id: { type: string }
        tool_calls: { type: array }
        function_call: { type: object }
    ChatCompletionRequest:
      type: object
      additionalProperties: false
      required: [model, messages]
      properties:
        model: { type: string }
        messages:
          type: array
          minItems: 1
          maxItems: 1000
          items: { $ref: '#/components/schemas/ChatMessage' }
        temperature: { type: number }
        top_p: { type: number }
        max_tokens: { type: integer, minimum: 1, maximum: 65536 }
        max_completion_tokens: { type: integer, minimum: 1, maximum: 65536 }
        stop:
          oneOf:
            - type: string
            - type: array
              items: { type: string }
        stream: { type: boolean }
        stream_options: { type: object, additionalProperties: true }
        tools: { type: array, items: { type: object, additionalProperties: true } }
        tool_choice: {}
        parallel_tool_calls: { type: boolean }
        response_format: { type: object, additionalProperties: true }
        reasoning_effort: { type: string }
        seed: { type: integer }
        frequency_penalty: { type: number }
        presence_penalty: { type: number }
        logit_bias: { type: object, additionalProperties: true }
        n: { type: integer, minimum: 1 }
        user: { type: string }
        functions: { type: array, items: { type: object, additionalProperties: true } }
        function_call: {}
      description: max_tokens and max_completion_tokens are mutually exclusive.
    ImageGenerationRequest:
      type: object
      additionalProperties: false
      required: [model, prompt]
      properties:
        model: { type: string, enum: [gpt-image-2, gpt-image-1.5] }
        prompt: { type: string, minLength: 1, maxLength: 32000 }
        quality: { type: string, enum: [low, medium, high], default: medium }
        size: { type: string, enum: [1024x1024, 1024x1536, 1536x1024], default: 1024x1024 }
        n: { const: 1, default: 1 }
        background: { type: string, enum: [transparent, opaque] }
        output_format: { type: string, enum: [png, jpeg, webp], default: png }
        output_compression: { type: integer, minimum: 0, maximum: 100 }
    ImageGenerationResponse:
      type: object
      required: [created, model, size, quality, output_format, data, keydealer_billing]
      properties:
        created: { type: integer }
        model: { type: string }
        size: { type: string }
        quality: { type: string }
        output_format: { type: string }
        data:
          type: array
          minItems: 1
          maxItems: 1
          items:
            type: object
            required: [b64_json]
            properties:
              b64_json: { type: string }
        keydealer_billing:
          type: object
          required: [operation_id, currency, charge_source, unit, image_count, charged_microrubles, charged_rub]
          properties:
            operation_id: { type: string }
            currency: { const: RUB }
            charge_source: { const: fixed_price }
            formula: { type: string }
            unit: { const: image }
            image_count: { const: 1 }
            charged_microrubles: { type: string, pattern: '^[0-9]+$' }
            charged_rub: { type: number, minimum: 0 }
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [message, type, code, request_id]
          properties:
            message: { type: string }
            type: { type: string }
            code:
              type: string
              enum:
                - invalid_request
                - unsupported_parameter
                - invalid_api_key
                - insufficient_balance
                - model_not_found
                - rate_limit_exceeded
                - model_service_unavailable
                - not_found
            request_id: { type: string }
  responses:
    ApiError:
      description: Stable KeyDealer-owned error envelope. Inspect error.code, not the localized message.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
