openapi: 3.0.3
info:
  title: GPU Share API
  version: 0.4.0
  description: |
    **GPU Share** - remote GPU inference marketplace (v3).

    - **Tenant:** Bearer API key `gpu_sk_...` - run ONNX on GPU using HTTPS URLs for model/inputs.
    - **Provider (executor):** Bearer agent token `gpu_at_...` - node polls `GET /v1/executor/work` (NAT-friendly).

    Interactive docs: `{baseUrl}/docs/` - spec: `{baseUrl}/openapi.yaml`

    Human-readable guide (Russian): see `docs/API.md` in the repository.

    ---

    ## Hello World (first invoke in 5 minutes)

    1. Check GPU availability:
       ```bash
       curl -s "$BASE/v1/marketplace/status"
       curl -s "$BASE/v1/nodes" -H "Authorization: Bearer $API_KEY"
       ```
       You need a node with `status: online` and `agent_status: idle`.

    2. Create a task (public MNIST ONNX; executor generates input if empty):
       ```bash
       curl -s -X POST "$BASE/v1/gpu/invoke" \
         -H "Authorization: Bearer $API_KEY" \
         -H "Content-Type: application/json" \
         -d '{
           "runtime": "onnx_cuda",
           "model_url": "https://github.com/onnx/models/raw/main/validated/vision/classification/mnist/model/mnist-12.onnx",
           "inputs_url": "",
           "timeout_sec": 600
         }'
       ```
       Save `id` from the response (`status: queued`).

    3. Poll status every **5-10 seconds** until `status` is `done`, `failed`, or `cancelled`:
       ```bash
       curl -s "$BASE/v1/gpu/invokes/$INVOKE_ID" -H "Authorization: Bearer $API_KEY"
       ```

    4. When `done`:
       - `result.outputs_url` - uploaded NPZ URL (if `outputs_upload` was set);
       - `result.outputs_inline_base64` - small inline outputs;
       - `result.metrics.device` - expect **`cuda`**;
       - on `failed` - read `error_message` and `GET /v1/gpu/invokes/{id}/logs`.

    5. Cancel: `DELETE /v1/gpu/invokes/{id}`.

    ---

    ## Invoke lifecycle

    | status | Meaning |
    |--------|---------|
    | `queued` | Waiting for a free GPU |
    | `running` | Executing on a node |
    | `done` | Success - see `result` |
    | `failed` | Error - see `error_message` and `/logs` |
    | `cancelled` | Cancelled by tenant |

    Recommended poll interval: **5-10 s**. Do not poll faster than 1/s.

    ---

    ## Errors

    All API errors: JSON `{"error": "<message>"}`.

    | HTTP | Typical cause |
    |------|----------------|
    | 401 | Missing or invalid Bearer token |
    | 404 | invoke/session/tenant not found |
    | 422 | Invalid artifact URL, runtime, or checksum |
    | 503 | No online GPU nodes |
    | 409 | Session busy |
    | 410 Gone | Session closed or expired |

servers:
  - url: https://gpu-share.ru
    description: Production
  - url: http://localhost
    description: Local gateway (docker compose)

tags:
  - name: system
    description: Health and metadata
  - name: marketplace
    description: Public marketplace status and registration
  - name: tenant
    description: Tenant profile and node catalog
  - name: gpu
    description: GPU invoke (ONNX, CatBoost)
  - name: artifacts
    description: Hosted artifact upload (no tenant nginx required)
  - name: sessions
    description: Long-lived GPU sessions (model in VRAM)
  - name: executor
    description: GPU node API (provider agent token)
  - name: admin
    description: Admin panel (Bearer ADMIN_SECRET)

paths:
  /health:
    get:
      tags: [system]
      summary: Liveness probe
      operationId: getHealth
      responses:
        "200":
          description: API process is up
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              example:
                status: ok

  /ready:
    get:
      tags: [system]
      summary: Readiness probe (includes DB)
      operationId: getReady
      responses:
        "200":
          description: Ready to serve traffic
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "503":
          $ref: "#/components/responses/Error503Ready"

  /v1/marketplace/status:
    get:
      tags: [marketplace]
      summary: Marketplace summary (no auth)
      operationId: getMarketplaceStatus
      responses:
        "200":
          description: Platform status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarketplaceStatusEnvelope"
              example:
                status: ok
                public_url: https://gpu-share.ru
                marketplace:
                  tenants_registered: 3
                  providers_registered: 2
                  nodes_online: 1
                  nodes_idle: 1
                  invokes_queued: 0

  /v1/public/register:
    post:
      tags: [marketplace]
      summary: Registration application (tenant or provider)
      operationId: publicRegister
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PublicRegisterRequest"
            example:
              type: tenant
              org_name: Acme ML
              contact_name: Alex Dev
              email: dev@acme.example
              phone: ""
              message: Need GPU API for ONNX inference
      responses:
        "201":
          description: Application accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRegisterResponse"
        "400":
          $ref: "#/components/responses/Error400"

  /v1/artifacts:
    get:
      tags: [artifacts]
      summary: List active hosted artifacts
      operationId: listArtifacts
      security:
        - tenantAuth: []
      responses:
        "200":
          description: Artifact list
          content:
            application/json:
              schema:
                type: object
                properties:
                  artifacts:
                    type: array
                    items:
                      $ref: "#/components/schemas/ArtifactMeta"
        "401":
          $ref: "#/components/responses/Error401"
    post:
      tags: [artifacts]
      summary: Upload model, inputs, or config
      description: |
        Upload file without your own HTTPS storage. Use returned `url` in `POST /v1/gpu/invoke`.

        Query: `kind=model|inputs|config`, optional `content_sha256` (required when CHECKSUM_MODE=enforce).
      operationId: uploadArtifact
      security:
        - tenantAuth: []
      parameters:
        - name: kind
          in: query
          required: true
          schema:
            type: string
            enum: [model, inputs, config]
        - name: content_sha256
          in: query
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "201":
          description: Uploaded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArtifactUploadResponse"
        "401":
          $ref: "#/components/responses/Error401"
        "413":
          description: File too large
        "422":
          $ref: "#/components/responses/Error422Invoke"
        "507":
          description: Tenant quota exceeded

  /v1/artifacts/{id}:
    delete:
      tags: [artifacts]
      summary: Delete hosted artifact
      operationId: deleteArtifact
      security:
        - tenantAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        "404":
          $ref: "#/components/responses/Error404"

  /v1/artifacts/{id}/download:
    get:
      tags: [artifacts]
      summary: Download artifact (executor, no auth)
      operationId: downloadArtifact
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: File bytes
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/Error404"
        "410":
          description: Expired

  /v1/gpu/probe-url:
    post:
      tags: [gpu]
      summary: Probe URL reachability (SSRF-safe)
      operationId: probeURL
      security:
        - tenantAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
      responses:
        "200":
          description: Probe result
          content:
            application/json:
              schema:
                type: object
                properties:
                  reachable:
                    type: boolean
                  http_status:
                    type: integer
                  latency_ms:
                    type: integer
                  error:
                    type: string

  /v1/tenant/me:
    get:
      tags: [tenant]
      summary: Current tenant profile
      operationId: getTenantMe
      security:
        - tenantAuth: []
      responses:
        "200":
          description: Tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tenant"
        "401":
          $ref: "#/components/responses/Error401"
        "404":
          $ref: "#/components/responses/Error404"

  /v1/nodes:
    get:
      tags: [tenant]
      summary: GPU nodes catalog (availability)
      operationId: listNodes
      security:
        - tenantAuth: []
      responses:
        "200":
          description: Nodes visible to tenant
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NodeCatalogList"
              example:
                nodes:
                  - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    status: online
                    gpu_model: NVIDIA GeForce RTX 4090
                    vram_total_mb: 24576
                    vram_free_mb: 22000
                    agent_status: idle
                    last_heartbeat_at: "2026-05-27T12:00:00Z"
        "401":
          $ref: "#/components/responses/Error401"

  /v1/gpu/runtimes:
    get:
      tags: [gpu]
      summary: Enabled inference runtimes
      operationId: listRuntimes
      security:
        - tenantAuth: []
      responses:
        "200":
          description: Runtimes catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RuntimeList"
              example:
                runtimes:
                  - runtime_id: onnx_cuda
                    enabled: true
                    max_timeout_sec: 7200
                    max_model_bytes: 2147483648
                    max_input_bytes: 536870912
                    description: ONNX Runtime with CUDA execution provider only
        "401":
          $ref: "#/components/responses/Error401"

  /v1/gpu/invoke:
    post:
      tags: [gpu]
      summary: Queue GPU task (ONNX or CatBoost)
      description: |
        Creates a task with `status: queued`. A free executor node picks it up via `/v1/executor/work`.

        - `model_url` - HTTPS link (or hosted `POST /v1/artifacts` URL).
        - `inputs_url` - optional NPZ; empty for ONNX smoke input.
        - `runtime` - `onnx_cuda` or `catboost_cuda` (train recipe in repo docs).
        - `outputs_upload` - presigned PUT where executor uploads the result.
      operationId: createInvoke
      security:
        - tenantAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvokeRequest"
            examples:
              mnist_smoke:
                summary: Public MNIST (no inputs)
                value:
                  runtime: onnx_cuda
                  model_url: https://github.com/onnx/models/raw/main/validated/vision/classification/mnist/model/mnist-12.onnx
                  inputs_url: ""
                  timeout_sec: 600
              with_upload:
                summary: With presigned output upload
                value:
                  runtime: onnx_cuda
                  model_url: https://storage.example.com/models/encoder.onnx
                  inputs_url: https://storage.example.com/inputs/batch.npz
                  timeout_sec: 1800
                  outputs_upload:
                    url: https://storage.example.com/results/out.npz?X-Amz-Signature=...
                    method: PUT
                    headers:
                      Content-Type: application/octet-stream
              catboost_train:
                summary: CatBoost train (hosted artifacts)
                value:
                  runtime: catboost_cuda
                  model_url: https://gpu-share.ru/v1/artifacts/art_config/download
                  inputs_url: https://gpu-share.ru/v1/artifacts/art_dataset/download
                  model_checksum_sha256: "0000000000000000000000000000000000000000000000000000000000000000"
                  inputs_checksum_sha256: "0000000000000000000000000000000000000000000000000000000000000000"
                  timeout_sec: 1200
      responses:
        "201":
          description: Invoke queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvokeCreateResponse"
              example:
                id: "8ef3588f-1234-5678-9abc-def012345678"
                runtime: onnx_cuda
                status: queued
                created_at: "2026-05-27T11:40:16Z"
        "400":
          $ref: "#/components/responses/Error400"
        "401":
          $ref: "#/components/responses/Error401"
        "422":
          $ref: "#/components/responses/Error422Invoke"
        "503":
          $ref: "#/components/responses/Error503NoGPU"

  /v1/gpu/invokes/{id}:
    get:
      tags: [gpu]
      summary: Get invoke status and result
      description: |
        Poll until `status` is terminal: `done`, `failed`, or `cancelled`.

        Result is only under `result` (not top-level):
        - `result.outputs_url`
        - `result.outputs_inline_base64`
        - `result.metrics.device` - for CUDA-only expect `cuda`
      operationId: getInvoke
      security:
        - tenantAuth: []
      parameters:
        - $ref: "#/components/parameters/InvokeId"
      responses:
        "200":
          description: Invoke task
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvokeTask"
              examples:
                queued:
                  summary: Queued
                  value:
                    id: "8ef3588f-1234-5678-9abc-def012345678"
                    runtime: onnx_cuda
                    status: queued
                    created_at: "2026-05-27T11:40:16Z"
                running:
                  summary: Running
                  value:
                    id: "8ef3588f-1234-5678-9abc-def012345678"
                    runtime: onnx_cuda
                    status: running
                    node_id: "node-uuid"
                    started_at: "2026-05-27T11:40:20Z"
                    created_at: "2026-05-27T11:40:16Z"
                done:
                  summary: Done with result
                  value:
                    id: "8ef3588f-1234-5678-9abc-def012345678"
                    runtime: onnx_cuda
                    status: done
                    node_id: "node-uuid"
                    result:
                      outputs_url: https://storage.example.com/results/out.npz
                      metrics:
                        device: cuda
                        vram_peak_mb: 4096
                        inference_ms: 1200
                        download_ms: 450
                    created_at: "2026-05-27T11:40:16Z"
                    started_at: "2026-05-27T11:40:20Z"
                    finished_at: "2026-05-27T11:40:22Z"
                failed:
                  summary: Failed
                  value:
                    id: "8ef3588f-1234-5678-9abc-def012345678"
                    runtime: onnx_cuda
                    status: failed
                    error_message: "CUDA execution failed"
                    created_at: "2026-05-27T11:40:16Z"
                    finished_at: "2026-05-27T11:40:25Z"
        "401":
          $ref: "#/components/responses/Error401"
        "404":
          $ref: "#/components/responses/Error404Invoke"
    delete:
      tags: [gpu]
      summary: Cancel invoke
      description: |
        - `queued` -> immediately `cancelled`
        - `running` -> cancel requested; executor checks `GET /v1/executor/invokes/{id}/cancel`
      operationId: cancelInvoke
      security:
        - tenantAuth: []
      parameters:
        - $ref: "#/components/parameters/InvokeId"
      responses:
        "200":
          description: Updated invoke (may still be running until executor aborts)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvokeTask"
        "401":
          $ref: "#/components/responses/Error401"
        "404":
          $ref: "#/components/responses/Error404Invoke"

  /v1/gpu/invokes/{id}/logs:
    get:
      tags: [gpu]
      summary: Executor logs (plain text)
      operationId: getInvokeLogs
      security:
        - tenantAuth: []
      parameters:
        - $ref: "#/components/parameters/InvokeId"
        - name: tail
          in: query
          description: Return only last N characters of log
          schema:
            type: integer
            minimum: 0
            example: 8000
      responses:
        "200":
          description: Plain text log tail
          content:
            text/plain:
              schema:
                type: string
              example: |
                [onnx] loading model from https://...
                [cuda] device=0 name=NVIDIA GeForce RTX 4090
                [infer] ok inference_ms=1200
        "401":
          $ref: "#/components/responses/Error401"
        "404":
          $ref: "#/components/responses/Error404Invoke"

  /v1/gpu/sessions:
    post:
      tags: [sessions]
      summary: Create GPU session (model pinned in VRAM)
      description: |
        Loads the model once; then `POST /v1/gpu/sessions/{id}/invoke` is faster for repeated forwards.

        With `CHECKSUM_MODE=enforce`, `model_checksum_sha256` is required.
      operationId: createSession
      security:
        - tenantAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionCreateRequest"
            example:
              runtime: onnx_cuda
              model_url: https://storage.example.com/models/encoder.onnx
              model_checksum_sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
              ttl_sec: 3600
              max_invokes: 100
      responses:
        "201":
          description: Session created (status loading -> ready)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionCreateResponse"
        "400":
          $ref: "#/components/responses/Error400"
        "401":
          $ref: "#/components/responses/Error401"
        "422":
          $ref: "#/components/responses/Error422"
        "503":
          $ref: "#/components/responses/Error503NoGPU"

  /v1/gpu/sessions/{id}:
    get:
      tags: [sessions]
      summary: Get session status
      operationId: getSession
      security:
        - tenantAuth: []
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GPUSession"
        "401":
          $ref: "#/components/responses/Error401"
        "404":
          $ref: "#/components/responses/Error404Session"
    delete:
      tags: [sessions]
      summary: Close session
      operationId: closeSession
      security:
        - tenantAuth: []
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session closed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GPUSession"
        "401":
          $ref: "#/components/responses/Error401"
        "404":
          $ref: "#/components/responses/Error404Session"
        "409":
          $ref: "#/components/responses/Error409SessionBusy"

  /v1/gpu/sessions/{id}/invoke:
    post:
      tags: [sessions]
      summary: Queue invoke inside session
      operationId: sessionInvoke
      security:
        - tenantAuth: []
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionInvokeRequest"
            example:
              inputs_url: https://storage.example.com/inputs/chunk.npz
              timeout_sec: 600
      responses:
        "201":
          description: Invoke queued for session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionInvokeResponse"
              example:
                invoke_id: "invoke-uuid"
                status: queued
        "401":
          $ref: "#/components/responses/Error401"
        "404":
          $ref: "#/components/responses/Error404Session"
        "409":
          $ref: "#/components/responses/Error409SessionNotReady"
        "410":
          $ref: "#/components/responses/Error410SessionGone"
        "422":
          $ref: "#/components/responses/Error422"

  /v1/executor/register:
    post:
      tags: [executor]
      summary: Register executor node
      description: Creates a node on first call; returns existing `node_id` on repeat.
      operationId: executorRegister
      security:
        - agentAuth: []
      responses:
        "201":
          description: New node created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecutorRegisterResponse"
        "200":
          description: Already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecutorRegisterResponse"
        "401":
          $ref: "#/components/responses/Error401"
        "403":
          $ref: "#/components/responses/Error403"

  /v1/executor/heartbeat:
    post:
      tags: [executor]
      summary: Node heartbeat
      operationId: executorHeartbeat
      security:
        - agentAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HeartbeatRequest"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "400":
          $ref: "#/components/responses/Error400"
        "401":
          $ref: "#/components/responses/Error401"
        "403":
          $ref: "#/components/responses/Error403"

  /v1/executor/work:
    get:
      tags: [executor]
      summary: Poll next work item (long-poll style)
      description: |
        Executor polls every 2-3 s. **204** - no work. **200** - take task and run.
      operationId: executorWork
      security:
        - agentAuth: []
      responses:
        "200":
          description: Work available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecutorWorkResponse"
        "204":
          description: No work (empty body)
        "401":
          $ref: "#/components/responses/Error401"

  /v1/executor/invokes/{id}/status:
    post:
      tags: [executor]
      summary: Report invoke status (executor)
      operationId: executorInvokeStatus
      security:
        - agentAuth: []
      parameters:
        - $ref: "#/components/parameters/InvokeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvokeStatusUpdate"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "400":
          $ref: "#/components/responses/Error400"
        "401":
          $ref: "#/components/responses/Error401"
        "403":
          $ref: "#/components/responses/Error403"
        "404":
          $ref: "#/components/responses/Error404Invoke"

  /v1/executor/invokes/{id}/result:
    post:
      tags: [executor]
      summary: Submit invoke result (executor)
      operationId: executorInvokeResult
      security:
        - agentAuth: []
      parameters:
        - $ref: "#/components/parameters/InvokeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvokeResultUpdate"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "400":
          $ref: "#/components/responses/Error400"
        "401":
          $ref: "#/components/responses/Error401"
        "403":
          $ref: "#/components/responses/Error403"
        "404":
          $ref: "#/components/responses/Error404Invoke"

  /v1/executor/invokes/{id}/logs:
    post:
      tags: [executor]
      summary: Append invoke logs (executor)
      operationId: executorInvokeLogs
      security:
        - agentAuth: []
      parameters:
        - $ref: "#/components/parameters/InvokeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogChunk"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "401":
          $ref: "#/components/responses/Error401"
        "403":
          $ref: "#/components/responses/Error403"
        "404":
          $ref: "#/components/responses/Error404Invoke"

  /v1/executor/invokes/{id}/cancel:
    get:
      tags: [executor]
      summary: Check if tenant requested cancel
      operationId: executorInvokeCancelCheck
      security:
        - agentAuth: []
      parameters:
        - $ref: "#/components/parameters/InvokeId"
      responses:
        "200":
          description: Cancel flag
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancelCheckResponse"
        "401":
          $ref: "#/components/responses/Error401"
        "403":
          $ref: "#/components/responses/Error403"
        "404":
          $ref: "#/components/responses/Error404Invoke"

  /v1/admin/dashboard:
    get:
      tags: [admin]
      summary: Admin dashboard metrics
      operationId: adminDashboard
      security:
        - adminAuth: []
      responses:
        "200":
          description: Dashboard
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Error401"
        "403":
          $ref: "#/components/responses/Error403"

  /v1/admin/invokes:
    get:
      tags: [admin]
      summary: List all invokes (admin)
      operationId: adminListInvokes
      security:
        - adminAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Invoke list with billing fields
          content:
            application/json:
              schema:
                type: object
                properties:
                  invokes:
                    type: array
                    items:
                      type: object
        "401":
          $ref: "#/components/responses/Error401"

components:
  securitySchemes:
    tenantAuth:
      type: http
      scheme: bearer
      description: Tenant API key (`gpu_sk_...`)
    agentAuth:
      type: http
      scheme: bearer
      description: Provider agent token (`gpu_at_...`)
    adminAuth:
      type: http
      scheme: bearer
      description: Marketplace ADMIN_SECRET

  parameters:
    InvokeId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    SessionId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Human-readable error message

    HealthResponse:
      type: object
      properties:
        status:
          type: string
          example: ok
        error:
          type: string
          description: Present on /ready when unhealthy

    MarketplaceStatus:
      type: object
      properties:
        tenants_registered:
          type: integer
        providers_registered:
          type: integer
        nodes_online:
          type: integer
        nodes_idle:
          type: integer
        invokes_queued:
          type: integer

    MarketplaceStatusEnvelope:
      type: object
      properties:
        status:
          type: string
        public_url:
          type: string
          format: uri
        marketplace:
          $ref: "#/components/schemas/MarketplaceStatus"

    PublicRegisterRequest:
      type: object
      required: [type, org_name, contact_name, email]
      properties:
        type:
          type: string
          enum: [tenant, provider]
        org_name:
          type: string
        contact_name:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        message:
          type: string

    PublicRegisterResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          example: pending
        message:
          type: string

    Tenant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time

    NodeCatalog:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [online, offline]
        gpu_model:
          type: string
          nullable: true
        vram_total_mb:
          type: integer
          nullable: true
        vram_free_mb:
          type: integer
          nullable: true
        agent_status:
          type: string
          enum: [idle, busy]
        last_heartbeat_at:
          type: string
          format: date-time
          nullable: true

    NodeCatalogList:
      type: object
      properties:
        nodes:
          type: array
          items:
            $ref: "#/components/schemas/NodeCatalog"

    RuntimeSpec:
      type: object
      properties:
        runtime_id:
          type: string
          example: onnx_cuda
        enabled:
          type: boolean
        max_timeout_sec:
          type: integer
        max_model_bytes:
          type: integer
          format: int64
        max_input_bytes:
          type: integer
          format: int64
        description:
          type: string

    ArtifactUploadResponse:
      type: object
      properties:
        id:
          type: string
          example: art_8ef3588f123456789abcdef012345678
        url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
        sha256:
          type: string
        size_bytes:
          type: integer

    ArtifactMeta:
      type: object
      properties:
        id:
          type: string
        kind:
          type: string
        sha256:
          type: string
        size_bytes:
          type: integer
        url:
          type: string
        expires_at:
          type: string

    RuntimeList:
      type: object
      properties:
        runtimes:
          type: array
          items:
            $ref: "#/components/schemas/RuntimeSpec"

    OutputsUpload:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          description: Presigned URL for executor to PUT results
        method:
          type: string
          default: PUT
          example: PUT
        headers:
          type: object
          additionalProperties:
            type: string

    InvokeRequest:
      type: object
      required: [model_url]
      properties:
        runtime:
          type: string
          default: onnx_cuda
          enum: [onnx_cuda, catboost_cuda]
          description: See docs/recipes/CATBOOST-TRAIN.md for catboost_cuda
        model_url:
          type: string
          format: uri
        inputs_url:
          type: string
          format: uri
          description: Optional NPZ/RAW; empty for auto-generated smoke input
        outputs_upload:
          $ref: "#/components/schemas/OutputsUpload"
        model_checksum_sha256:
          type: string
          pattern: "^[a-fA-F0-9]{64}$"
        inputs_checksum_sha256:
          type: string
          pattern: "^[a-fA-F0-9]{64}$"
        timeout_sec:
          type: integer
          default: 3600
          minimum: 1
        input_format:
          type: string
          enum: [npz, raw]
        metadata:
          type: object
          additionalProperties: true

    InvokeCreateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        runtime:
          type: string
        status:
          type: string
          enum: [queued]
        created_at:
          type: string
          format: date-time

    InvokeMetrics:
      type: object
      properties:
        device:
          type: string
          description: Expect `cuda` for onnx_cuda runtime
          example: cuda
        vram_peak_mb:
          type: integer
        inference_ms:
          type: integer
          format: int64
        download_ms:
          type: integer
          format: int64
        checksum_verified:
          type: boolean
          nullable: true

    InvokeResult:
      type: object
      properties:
        outputs_url:
          type: string
          format: uri
        outputs_inline_base64:
          type: string
          description: Small outputs returned inline (base64)
        metrics:
          $ref: "#/components/schemas/InvokeMetrics"

    InvokeTask:
      type: object
      description: Full invoke state for polling
      properties:
        id:
          type: string
          format: uuid
        runtime:
          type: string
        status:
          type: string
          enum: [queued, running, done, failed, cancelled]
        request:
          $ref: "#/components/schemas/InvokeRequest"
        result:
          $ref: "#/components/schemas/InvokeResult"
        node_id:
          type: string
          format: uuid
          nullable: true
        session_id:
          type: string
          format: uuid
          nullable: true
        cancel_requested_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true

    SessionCreateRequest:
      type: object
      required: [model_url]
      properties:
        runtime:
          type: string
          default: onnx_cuda
        model_url:
          type: string
          format: uri
        model_checksum_sha256:
          type: string
        ttl_sec:
          type: integer
          default: 3600
        max_invokes:
          type: integer
          default: 1000

    SessionCreateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [loading, ready, failed]
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    GPUSession:
      type: object
      properties:
        id:
          type: string
          format: uuid
        runtime:
          type: string
        status:
          type: string
          enum: [loading, ready, failed, closed, expired]
        model_url:
          type: string
          format: uri
        model_checksum_sha256:
          type: string
        node_id:
          type: string
          format: uuid
          nullable: true
        ttl_sec:
          type: integer
        max_invokes:
          type: integer
        invoke_count:
          type: integer
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        closed_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true

    SessionInvokeRequest:
      type: object
      properties:
        inputs_url:
          type: string
          format: uri
        inputs_checksum_sha256:
          type: string
        timeout_sec:
          type: integer
          default: 600

    SessionInvokeResponse:
      type: object
      properties:
        invoke_id:
          type: string
          format: uuid
        status:
          type: string
          example: queued

    HeartbeatRequest:
      type: object
      required: [node_id]
      properties:
        node_id:
          type: string
          format: uuid
        gpu_model:
          type: string
        vram_total_mb:
          type: integer
        vram_free_mb:
          type: integer
        driver_version:
          type: string
        agent_status:
          type: string
          enum: [idle, busy]
        agent_version:
          type: string

    ExecutorRegisterResponse:
      type: object
      properties:
        node_id:
          type: string
          format: uuid
        provider_id:
          type: string
          format: uuid
        status:
          type: string
          description: Present when re-registering existing node

    ExecutorWorkResponse:
      type: object
      properties:
        invoke_id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        runtime:
          type: string
        request:
          $ref: "#/components/schemas/InvokeRequest"
        timeout_sec:
          type: integer

    InvokeStatusUpdate:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [running, failed, cancelled]
        error_message:
          type: string
          nullable: true

    InvokeResultUpdate:
      type: object
      required: [result]
      properties:
        result:
          $ref: "#/components/schemas/InvokeResult"

    LogChunk:
      type: object
      properties:
        chunk:
          type: string
        append:
          type: boolean
          default: true

    CancelCheckResponse:
      type: object
      properties:
        cancel_requested:
          type: boolean

  responses:
    Error400:
      description: Bad request (invalid JSON or missing field)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            invalid_json:
              value:
                error: invalid json
            missing_field:
              value:
                error: model_url required

    Error401:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: unauthorized

    Error403:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: forbidden

    Error404:
      description: Not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: not found

    Error404Invoke:
      description: Invoke not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: invoke not found

    Error404Session:
      description: Session not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: session not found

    Error409SessionBusy:
      description: Session has running invoke
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: session busy

    Error409SessionNotReady:
      description: Session not ready yet
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: session not ready

    Error410SessionGone:
      description: Session closed or expired
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: session expired

    Error422:
      description: Validation error (URL, runtime, checksum)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "artifact URL must use https scheme"

    Error422Invoke:
      description: Invalid invoke request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            bad_url:
              value:
                error: "artifact URL host not allowed"
            runtime:
              value:
                error: runtime not found
            checksum:
              value:
                error: model_checksum_sha256 required

    Error503NoGPU:
      description: No online GPU nodes available
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: no online gpu nodes

    Error503Ready:
      description: Service not ready (database unavailable)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            status: unavailable
            error: connection refused
