openapi: 3.0.3

info:
  title: Octomil - On-Device AI Platform API
  version: 1.0.0
  description: |
    REST API for deploying, routing, and operating AI models across edge devices
    with staged rollouts, fleet monitoring, and cloud fallback.

    Devices register with the server, download models, report telemetry, and
    receive routing decisions. Enterprise endpoints also support federated
    learning workflows when enabled.

    ## Authentication

    All endpoints (except `/api/v1/health`) require a Bearer token in the
    `Authorization` header. Tokens follow the Stripe-style format
    `sk_live_<32_random_chars>`. A device receives its API key upon
    registration.

    ```
    Authorization: Bearer sk_live_abc123...
    ```

    ## Rate Limiting

    Authenticated endpoints are rate-limited to **100 requests per minute**
    per device. When the limit is exceeded the server responds with
    `429 Too Many Requests` and a `Retry-After` header.

  contact:
    name: Octomil Team
    url: https://github.com/your-org/octomil
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: http://localhost:8000
    description: Local development server

tags:
  - name: Health
    description: Server health and status checks
  - name: Devices
    description: Device registration, heartbeat, and management
  - name: Rounds
    description: Federated learning training round lifecycle
  - name: Model Updates
    description: Local model update submission
  - name: Models
    description: Model catalog, versioning, upload, and download

security:
  - bearerAuth: []

paths:
  # ---------------------------------------------------------------------------
  # Health
  # ---------------------------------------------------------------------------
  /api/v1/health:
    get:
      operationId: healthCheck
      summary: Health check
      description: |
        Returns basic server liveness information. This endpoint does **not**
        require authentication and is intended for load-balancer probes.
      tags:
        - Health
      security: []
      responses:
        "200":
          description: Server is healthy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              example:
                status: healthy
                version: "1.0.0"
                timestamp: "2025-07-01T12:00:00Z"

  /api/v1/status:
    get:
      operationId: systemStatus
      summary: System status
      description: |
        Returns detailed system status including counts of active devices,
        current round information, and storage utilization. Requires
        authentication.
      tags:
        - Health
      responses:
        "200":
          description: System status retrieved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusResponse"
              example:
                status: operational
                active_devices: 42
                total_devices: 78
                current_round_number: 17
                current_round_status: active
                total_models: 18
                timestamp: "2025-07-01T12:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ---------------------------------------------------------------------------
  # Devices
  # ---------------------------------------------------------------------------
  /api/v1/devices/register:
    post:
      operationId: registerDevice
      summary: Register a new device
      description: |
        Registers a new device with the orchestration server. The server
        generates a unique API key and returns it in the response. The client
        **must** store this key -- it cannot be retrieved again.

        If a device with the same `device_id` already exists the server
        returns `409 Conflict`.
      tags:
        - Devices
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeviceRegisterRequest"
            example:
              device_id: laptop-001
              platform: python
              capabilities:
                cpu: "4-core"
                memory_gb: 8
                gpu: false
      responses:
        "201":
          description: Device registered successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeviceRegisterResponse"
              example:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                device_id: laptop-001
                api_key: "sk_live_EXAMPLE_KEY_DO_NOT_USE_0001"
                platform: python
                status: active
                created_at: "2025-07-01T12:00:00Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          description: A device with this `device_id` already exists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: conflict
                message: "Device with device_id 'laptop-001' already exists."
                status_code: 409
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/v1/devices/{device_id}:
    get:
      operationId: getDevice
      summary: Get device information
      description: |
        Retrieves detailed information about a registered device, including
        its platform, capabilities, status, and last heartbeat timestamp.
      tags:
        - Devices
      parameters:
        - $ref: "#/components/parameters/DeviceIdPath"
      responses:
        "200":
          description: Device information retrieved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Device"
              example:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                device_id: laptop-001
                platform: python
                capabilities:
                  cpu: "4-core"
                  memory_gb: 8
                  gpu: false
                status: active
                last_heartbeat: "2025-07-01T12:05:00Z"
                created_at: "2025-07-01T12:00:00Z"
                updated_at: "2025-07-01T12:05:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/v1/devices/{device_id}/heartbeat:
    put:
      operationId: deviceHeartbeat
      summary: Device heartbeat
      description: |
        Updates the device heartbeat timestamp and optionally reports device
        metrics. The server may include instructions in the response, such as
        a pending training round the device should participate in.
      tags:
        - Devices
      parameters:
        - $ref: "#/components/parameters/DeviceIdPath"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HeartbeatRequest"
            example:
              status: active
              metrics:
                cpu_usage_percent: 42.5
                memory_usage_percent: 61.0
                battery_level: 85
      responses:
        "200":
          description: Heartbeat acknowledged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HeartbeatResponse"
              example:
                status: active
                last_heartbeat: "2025-07-01T12:10:00Z"
                pending_round_id: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
                message: "Training round 18 is awaiting your participation."
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ---------------------------------------------------------------------------
  # Rounds
  # ---------------------------------------------------------------------------
  /api/v1/training/rounds/start:
    post:
      operationId: startRound
      summary: Start a training round
      description: |
        Initializes a new federated learning training round. The server
        selects devices, sets the round to `active`, and distributes the
        current global model to participants.

        Only one round may be active at a time. If a round is already in
        progress the server returns `409 Conflict`.
      tags:
        - Rounds
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RoundStartRequest"
            example:
              target_devices: 10
              min_devices: 8
              timeout_seconds: 300
              global_model_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      responses:
        "201":
          description: Training round started.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Round"
              example:
                id: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
                round_number: 18
                status: active
                target_devices: 10
                min_devices: 8
                participated_devices: 0
                completed_devices: 0
                failed_devices: 0
                global_model_path: "s3://octomil-models/global/round_17.pt"
                aggregated_model_path: null
                started_at: "2025-07-01T12:15:00Z"
                completed_at: null
                timeout_at: "2025-07-01T12:20:00Z"
                created_at: "2025-07-01T12:15:00Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: A training round is already in progress.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: conflict
                message: "Round 17 is currently active. Wait for it to complete or cancel it."
                status_code: 409
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/v1/training/rounds/current:
    get:
      operationId: getCurrentRound
      summary: Get current round
      description: |
        Returns the most recent training round regardless of status. This is
        a convenience endpoint equivalent to fetching the round with the
        highest `round_number`.
      tags:
        - Rounds
      responses:
        "200":
          description: Current round retrieved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Round"
              example:
                id: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
                round_number: 18
                status: active
                target_devices: 10
                min_devices: 8
                participated_devices: 6
                completed_devices: 4
                failed_devices: 1
                global_model_path: "s3://octomil-models/global/round_17.pt"
                aggregated_model_path: null
                started_at: "2025-07-01T12:15:00Z"
                completed_at: null
                timeout_at: "2025-07-01T12:20:00Z"
                created_at: "2025-07-01T12:15:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: No rounds have been created yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: not_found
                message: "No training rounds exist yet."
                status_code: 404
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/v1/training/rounds/{round_id}/status:
    get:
      operationId: getRoundStatus
      summary: Get round status
      description: |
        Returns the full status of a training round including participant
        progress counters and timing information.
      tags:
        - Rounds
      parameters:
        - $ref: "#/components/parameters/RoundIdPath"
      responses:
        "200":
          description: Round status retrieved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Round"
              example:
                id: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
                round_number: 18
                status: aggregating
                target_devices: 10
                min_devices: 8
                participated_devices: 10
                completed_devices: 9
                failed_devices: 1
                global_model_path: "s3://octomil-models/global/round_17.pt"
                aggregated_model_path: null
                started_at: "2025-07-01T12:15:00Z"
                completed_at: null
                timeout_at: "2025-07-01T12:20:00Z"
                created_at: "2025-07-01T12:15:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ---------------------------------------------------------------------------
  # Model Updates (submitted by devices during a round)
  # ---------------------------------------------------------------------------
  /api/v1/training/rounds/{round_id}/updates:
    post:
      operationId: submitModelUpdate
      summary: Submit a model update
      description: |
        A device submits its locally-trained model update for the given
        round. The request body contains training metadata; the model weights
        file is uploaded as a multipart form field.

        The server validates that the device is a participant in the round
        and that the round is still accepting updates (`status = active`).
      tags:
        - Model Updates
      parameters:
        - $ref: "#/components/parameters/RoundIdPath"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ModelUpdateUpload"
      responses:
        "201":
          description: Model update accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelUpdate"
              example:
                id: "c3d4e5f6-a7b8-9012-cdef-345678901234"
                round_id: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
                device_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                model_path: "s3://octomil-models/updates/round_18/device_a1b2c3d4.pt"
                model_size_bytes: 4521984
                num_samples: 5000
                local_loss: 0.342100
                local_accuracy: 0.8915
                training_time_seconds: 47
                created_at: "2025-07-01T12:18:30Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            The round is not accepting updates (already aggregating, completed,
            or failed), or this device already submitted an update.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: conflict
                message: "Round 18 is no longer accepting updates (status: aggregating)."
                status_code: 409
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ---------------------------------------------------------------------------
  # Models
  # ---------------------------------------------------------------------------
  /api/v1/models:
    post:
      operationId: createModel
      summary: Create a model
      description: |
        Creates a new model entry in the catalog. This registers the model
        metadata; the actual weights are uploaded separately via the version
        upload endpoint.
      tags:
        - Models
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ModelCreateRequest"
            example:
              framework: pytorch
              framework_version: "2.1.0"
              description: "MNIST digit classifier - 3-layer CNN"
              is_baseline: true
      responses:
        "201":
          description: Model created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Model"
              example:
                id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                round_id: null
                model_path: null
                model_size_bytes: null
                model_hash: null
                framework: pytorch
                framework_version: "2.1.0"
                accuracy: null
                loss: null
                is_baseline: true
                description: "MNIST digit classifier - 3-layer CNN"
                created_at: "2025-07-01T11:00:00Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

    get:
      operationId: listModels
      summary: List models
      description: |
        Returns a paginated list of models in the catalog, ordered by
        creation time descending (newest first).
      tags:
        - Models
      parameters:
        - name: limit
          in: query
          description: Maximum number of models to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          description: Number of models to skip for pagination.
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: framework
          in: query
          description: Filter by ML framework.
          schema:
            type: string
            enum:
              - pytorch
              - tensorflow
              - xgboost
              - sklearn
        - name: is_baseline
          in: query
          description: Filter by baseline flag.
          schema:
            type: boolean
      responses:
        "200":
          description: List of models.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelListResponse"
              example:
                models:
                  - id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                    round_id: null
                    model_path: "s3://octomil-models/baseline/model_v1.pt"
                    model_size_bytes: 4521984
                    model_hash: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
                    framework: pytorch
                    framework_version: "2.1.0"
                    accuracy: 0.8200
                    loss: 0.530000
                    is_baseline: true
                    description: "MNIST digit classifier - 3-layer CNN"
                    created_at: "2025-07-01T11:00:00Z"
                total: 18
                limit: 20
                offset: 0
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/v1/models/{model_id}:
    get:
      operationId: getModel
      summary: Get a model
      description: |
        Retrieves a single model by ID, including its full metadata and
        version history.
      tags:
        - Models
      parameters:
        - $ref: "#/components/parameters/ModelIdPath"
      responses:
        "200":
          description: Model retrieved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelDetail"
              example:
                id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                round_id: null
                model_path: "s3://octomil-models/baseline/model_v1.pt"
                model_size_bytes: 4521984
                model_hash: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
                framework: pytorch
                framework_version: "2.1.0"
                accuracy: 0.8200
                loss: 0.530000
                is_baseline: true
                description: "MNIST digit classifier - 3-layer CNN"
                created_at: "2025-07-01T11:00:00Z"
                versions:
                  - version: 1
                    format: pytorch
                    model_size_bytes: 4521984
                    uploaded_at: "2025-07-01T11:00:00Z"
                  - version: 2
                    format: onnx
                    model_size_bytes: 4100096
                    uploaded_at: "2025-07-01T13:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/v1/models/{model_id}/versions/upload:
    post:
      operationId: uploadModelVersion
      summary: Upload a model version
      description: |
        Uploads model weights as a new version. The server stores the file
        in S3/MinIO and optionally converts it to ONNX format. Returns the
        created version metadata.
      tags:
        - Models
      parameters:
        - $ref: "#/components/parameters/ModelIdPath"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: Model weights file (.pt, .h5, .pkl, .onnx).
                format:
                  type: string
                  enum:
                    - pytorch
                    - tensorflow
                    - onnx
                    - sklearn
                    - xgboost
                  description: Source format of the uploaded model.
                description:
                  type: string
                  description: Optional human-readable description of this version.
      responses:
        "201":
          description: Model version uploaded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModelVersion"
              example:
                model_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                version: 3
                format: pytorch
                model_path: "s3://octomil-models/b2c3d4e5/v3/model.pt"
                model_size_bytes: 4521984
                model_hash: "sha256:a1b2c3d4..."
                description: "After round 18 aggregation"
                uploaded_at: "2025-07-01T13:30:00Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/v1/models/{model_id}/versions/{version}/download:
    get:
      operationId: downloadModelVersion
      summary: Download a model version
      description: |
        Downloads the model weights for the specified version. An optional
        `format` query parameter requests a converted artifact (e.g., ONNX,
        TFLite, CoreML). If the requested format is not yet available the
        server returns `404`.
      tags:
        - Models
      parameters:
        - $ref: "#/components/parameters/ModelIdPath"
        - name: version
          in: path
          required: true
          description: Model version number.
          schema:
            type: integer
            minimum: 1
        - name: format
          in: query
          description: Desired artifact format. Defaults to the original upload format.
          schema:
            type: string
            enum:
              - pytorch
              - onnx
              - tflite
              - coreml
      responses:
        "200":
          description: Model binary returned.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
          headers:
            Content-Disposition:
              description: Suggested filename for the download.
              schema:
                type: string
                example: 'attachment; filename="model_v3.onnx"'
            Content-Length:
              description: File size in bytes.
              schema:
                type: integer
                example: 4521984
            X-Model-Hash:
              description: SHA-256 hash of the file for integrity verification.
              schema:
                type: string
                example: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

components:
  # ---------------------------------------------------------------------------
  # Security Schemes
  # ---------------------------------------------------------------------------
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "sk_live_*"
      description: |
        API key issued during device registration. Pass it as a Bearer token:
        `Authorization: Bearer sk_live_...`

  # ---------------------------------------------------------------------------
  # Reusable Parameters
  # ---------------------------------------------------------------------------
  parameters:
    DeviceIdPath:
      name: device_id
      in: path
      required: true
      description: Unique device identifier (the user-provided `device_id`, not the internal UUID).
      schema:
        type: string
        example: laptop-001

    RoundIdPath:
      name: round_id
      in: path
      required: true
      description: UUID of the training round.
      schema:
        type: string
        format: uuid
        example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"

    ModelIdPath:
      name: model_id
      in: path
      required: true
      description: UUID of the model.
      schema:
        type: string
        format: uuid
        example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"

  # ---------------------------------------------------------------------------
  # Reusable Responses
  # ---------------------------------------------------------------------------
  responses:
    BadRequest:
      description: The request body is invalid or missing required fields.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: bad_request
            message: "Validation failed: 'device_id' is required."
            status_code: 400

    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: unauthorized
            message: "Invalid or missing API key."
            status_code: 401

    NotFound:
      description: The requested resource was not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: not_found
            message: "Resource not found."
            status_code: 404

    TooManyRequests:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 30
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: rate_limited
            message: "Rate limit exceeded. Try again in 30 seconds."
            status_code: 429

    InternalServerError:
      description: An unexpected server error occurred.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: internal_error
            message: "An unexpected error occurred. Please try again later."
            status_code: 500

  # ---------------------------------------------------------------------------
  # Schemas
  # ---------------------------------------------------------------------------
  schemas:
    # -- Error ----------------------------------------------------------------
    Error:
      type: object
      required:
        - error
        - message
        - status_code
      properties:
        error:
          type: string
          description: Machine-readable error code.
          example: bad_request
        message:
          type: string
          description: Human-readable error description.
          example: "Validation failed: 'device_id' is required."
        status_code:
          type: integer
          description: HTTP status code.
          example: 400

    # -- Health ---------------------------------------------------------------
    HealthResponse:
      type: object
      required:
        - status
        - version
        - timestamp
      properties:
        status:
          type: string
          enum:
            - healthy
            - degraded
          example: healthy
        version:
          type: string
          description: Server software version.
          example: "1.0.0"
        timestamp:
          type: string
          format: date-time
          description: Current server time (UTC).
          example: "2025-07-01T12:00:00Z"

    StatusResponse:
      type: object
      required:
        - status
        - active_devices
        - total_devices
        - total_models
        - timestamp
      properties:
        status:
          type: string
          enum:
            - operational
            - degraded
            - maintenance
          example: operational
        active_devices:
          type: integer
          description: Devices with a heartbeat in the last 5 minutes.
          example: 42
        total_devices:
          type: integer
          description: Total registered devices.
          example: 78
        current_round_number:
          type: integer
          nullable: true
          description: Current or most recent round number, null if no rounds exist.
          example: 17
        current_round_status:
          type: string
          nullable: true
          description: Status of the current round.
          enum:
            - pending
            - active
            - aggregating
            - completed
            - failed
          example: active
        total_models:
          type: integer
          description: Total models in the catalog.
          example: 18
        timestamp:
          type: string
          format: date-time
          example: "2025-07-01T12:00:00Z"

    # -- Devices --------------------------------------------------------------
    DeviceRegisterRequest:
      type: object
      required:
        - device_id
      properties:
        device_id:
          type: string
          minLength: 1
          maxLength: 255
          description: Unique, user-chosen identifier for the device.
          example: laptop-001
        platform:
          type: string
          enum:
            - python
            - ios
            - android
            - edge
          description: Device platform. Defaults to `python` if omitted.
          example: python
        capabilities:
          type: object
          additionalProperties: true
          description: |
            Free-form JSON describing device hardware. Common keys include
            `cpu`, `memory_gb`, `gpu`, `storage_gb`.
          example:
            cpu: "4-core"
            memory_gb: 8
            gpu: false

    DeviceRegisterResponse:
      type: object
      required:
        - id
        - device_id
        - api_key
        - status
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: Internal UUID assigned by the server.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        device_id:
          type: string
          description: The user-provided device identifier.
          example: laptop-001
        api_key:
          type: string
          description: |
            API key for this device. Store it securely -- it cannot be
            retrieved again.
          example: "sk_live_EXAMPLE_KEY_DO_NOT_USE_0001"
        platform:
          type: string
          enum:
            - python
            - ios
            - android
            - edge
          example: python
        status:
          type: string
          enum:
            - active
            - inactive
            - offline
          example: active
        created_at:
          type: string
          format: date-time
          example: "2025-07-01T12:00:00Z"

    Device:
      type: object
      required:
        - id
        - device_id
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        device_id:
          type: string
          example: laptop-001
        platform:
          type: string
          enum:
            - python
            - ios
            - android
            - edge
          example: python
        capabilities:
          type: object
          additionalProperties: true
          example:
            cpu: "4-core"
            memory_gb: 8
            gpu: false
        status:
          type: string
          enum:
            - active
            - inactive
            - offline
          example: active
        last_heartbeat:
          type: string
          format: date-time
          nullable: true
          description: Last heartbeat timestamp, null if the device has never sent one.
          example: "2025-07-01T12:05:00Z"
        created_at:
          type: string
          format: date-time
          example: "2025-07-01T12:00:00Z"
        updated_at:
          type: string
          format: date-time
          example: "2025-07-01T12:05:00Z"

    HeartbeatRequest:
      type: object
      properties:
        status:
          type: string
          enum:
            - active
            - inactive
          description: Reported device status.
          example: active
        metrics:
          type: object
          additionalProperties: true
          description: Optional device metrics snapshot.
          example:
            cpu_usage_percent: 42.5
            memory_usage_percent: 61.0
            battery_level: 85

    HeartbeatResponse:
      type: object
      required:
        - status
        - last_heartbeat
      properties:
        status:
          type: string
          enum:
            - active
            - inactive
            - offline
          example: active
        last_heartbeat:
          type: string
          format: date-time
          example: "2025-07-01T12:10:00Z"
        pending_round_id:
          type: string
          format: uuid
          nullable: true
          description: |
            If a training round is waiting for this device, the round UUID is
            returned here. Null if no round is pending.
          example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
        message:
          type: string
          nullable: true
          description: Optional human-readable instruction from the server.
          example: "Training round 18 is awaiting your participation."

    # -- Rounds ---------------------------------------------------------------
    RoundStartRequest:
      type: object
      required:
        - target_devices
        - min_devices
      properties:
        target_devices:
          type: integer
          minimum: 1
          description: Number of devices to select for this round.
          example: 10
        min_devices:
          type: integer
          minimum: 1
          description: |
            Minimum number of device updates required before aggregation
            proceeds. Must be less than or equal to `target_devices`.
          example: 8
        timeout_seconds:
          type: integer
          minimum: 60
          maximum: 3600
          default: 300
          description: Seconds before the round times out.
          example: 300
        global_model_id:
          type: string
          format: uuid
          description: |
            UUID of the global model to distribute. If omitted the server
            uses the latest aggregated model (or baseline if no rounds have
            completed).
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"

    Round:
      type: object
      required:
        - id
        - round_number
        - status
        - target_devices
        - min_devices
        - participated_devices
        - completed_devices
        - failed_devices
        - created_at
      properties:
        id:
          type: string
          format: uuid
          example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
        round_number:
          type: integer
          description: Sequential round counter.
          example: 18
        status:
          type: string
          enum:
            - pending
            - active
            - aggregating
            - completed
            - failed
          example: active
        target_devices:
          type: integer
          example: 10
        min_devices:
          type: integer
          example: 8
        participated_devices:
          type: integer
          description: Devices that have begun local training.
          example: 6
        completed_devices:
          type: integer
          description: Devices that have submitted updates.
          example: 4
        failed_devices:
          type: integer
          description: Devices that failed during training.
          example: 1
        global_model_path:
          type: string
          nullable: true
          description: S3/MinIO path to the global model distributed for this round.
          example: "s3://octomil-models/global/round_17.pt"
        aggregated_model_path:
          type: string
          nullable: true
          description: S3/MinIO path to the aggregated model after completion.
          example: null
        started_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-07-01T12:15:00Z"
        completed_at:
          type: string
          format: date-time
          nullable: true
          example: null
        timeout_at:
          type: string
          format: date-time
          nullable: true
          description: When the round will be marked failed due to timeout.
          example: "2025-07-01T12:20:00Z"
        created_at:
          type: string
          format: date-time
          example: "2025-07-01T12:15:00Z"

    # -- Model Updates --------------------------------------------------------
    ModelUpdateUpload:
      type: object
      required:
        - file
        - num_samples
      properties:
        file:
          type: string
          format: binary
          description: Serialized model weights file.
        num_samples:
          type: integer
          minimum: 1
          description: Number of local training samples used.
          example: 5000
        local_loss:
          type: number
          format: double
          description: Final local training loss.
          example: 0.3421
        local_accuracy:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: Final local training accuracy (0.0 - 1.0).
          example: 0.8915
        training_time_seconds:
          type: integer
          minimum: 0
          description: Wall-clock training duration in seconds.
          example: 47

    ModelUpdate:
      type: object
      required:
        - id
        - round_id
        - device_id
        - model_path
        - num_samples
        - created_at
      properties:
        id:
          type: string
          format: uuid
          example: "c3d4e5f6-a7b8-9012-cdef-345678901234"
        round_id:
          type: string
          format: uuid
          example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
        device_id:
          type: string
          format: uuid
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        model_path:
          type: string
          description: S3/MinIO path where the update is stored.
          example: "s3://octomil-models/updates/round_18/device_a1b2c3d4.pt"
        model_size_bytes:
          type: integer
          format: int64
          nullable: true
          description: Size of the uploaded model file.
          example: 4521984
        num_samples:
          type: integer
          description: Number of local training samples used.
          example: 5000
        local_loss:
          type: number
          format: double
          nullable: true
          example: 0.342100
        local_accuracy:
          type: number
          format: double
          nullable: true
          example: 0.8915
        training_time_seconds:
          type: integer
          nullable: true
          example: 47
        created_at:
          type: string
          format: date-time
          example: "2025-07-01T12:18:30Z"

    # -- Models ---------------------------------------------------------------
    ModelCreateRequest:
      type: object
      properties:
        framework:
          type: string
          enum:
            - pytorch
            - tensorflow
            - xgboost
            - sklearn
          description: ML framework the model was built with.
          example: pytorch
        framework_version:
          type: string
          description: Version of the ML framework.
          example: "2.1.0"
        description:
          type: string
          maxLength: 1000
          description: Human-readable model description.
          example: "MNIST digit classifier - 3-layer CNN"
        is_baseline:
          type: boolean
          default: false
          description: Whether this is a baseline (initial) model.
          example: true

    Model:
      type: object
      required:
        - id
        - is_baseline
        - created_at
      properties:
        id:
          type: string
          format: uuid
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        round_id:
          type: string
          format: uuid
          nullable: true
          description: Round that produced this model (null for baselines).
          example: null
        model_path:
          type: string
          nullable: true
          description: S3/MinIO storage path.
          example: "s3://octomil-models/baseline/model_v1.pt"
        model_size_bytes:
          type: integer
          format: int64
          nullable: true
          example: 4521984
        model_hash:
          type: string
          nullable: true
          description: SHA-256 hash for integrity verification.
          example: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        framework:
          type: string
          nullable: true
          enum:
            - pytorch
            - tensorflow
            - xgboost
            - sklearn
          example: pytorch
        framework_version:
          type: string
          nullable: true
          example: "2.1.0"
        accuracy:
          type: number
          format: double
          nullable: true
          description: Evaluation accuracy (0.0 - 1.0).
          example: 0.8200
        loss:
          type: number
          format: double
          nullable: true
          description: Evaluation loss.
          example: 0.530000
        is_baseline:
          type: boolean
          example: true
        description:
          type: string
          nullable: true
          example: "MNIST digit classifier - 3-layer CNN"
        created_at:
          type: string
          format: date-time
          example: "2025-07-01T11:00:00Z"

    ModelDetail:
      description: Full model information including version history.
      allOf:
        - $ref: "#/components/schemas/Model"
        - type: object
          properties:
            versions:
              type: array
              description: List of uploaded versions for this model.
              items:
                $ref: "#/components/schemas/ModelVersionSummary"

    ModelVersionSummary:
      type: object
      required:
        - version
        - format
        - uploaded_at
      properties:
        version:
          type: integer
          description: Sequential version number.
          example: 1
        format:
          type: string
          description: Format of the stored artifact.
          example: pytorch
        model_size_bytes:
          type: integer
          format: int64
          nullable: true
          example: 4521984
        uploaded_at:
          type: string
          format: date-time
          example: "2025-07-01T11:00:00Z"

    ModelVersion:
      type: object
      required:
        - model_id
        - version
        - format
        - model_path
        - uploaded_at
      properties:
        model_id:
          type: string
          format: uuid
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        version:
          type: integer
          example: 3
        format:
          type: string
          example: pytorch
        model_path:
          type: string
          example: "s3://octomil-models/b2c3d4e5/v3/model.pt"
        model_size_bytes:
          type: integer
          format: int64
          nullable: true
          example: 4521984
        model_hash:
          type: string
          nullable: true
          example: "sha256:a1b2c3d4..."
        description:
          type: string
          nullable: true
          example: "After round 18 aggregation"
        uploaded_at:
          type: string
          format: date-time
          example: "2025-07-01T13:30:00Z"

    ModelListResponse:
      type: object
      required:
        - models
        - total
        - limit
        - offset
      properties:
        models:
          type: array
          items:
            $ref: "#/components/schemas/Model"
        total:
          type: integer
          description: Total number of models matching the filter.
          example: 18
        limit:
          type: integer
          description: Maximum items returned per page.
          example: 20
        offset:
          type: integer
          description: Current pagination offset.
          example: 0
