openapi: 3.1.0
info:
  title: SorryAssets API
  description: |
    SorryAssets is a multi-provider AIGC generation gateway with a typed provenance graph.

    This API is for both human clients (desktop, web) and AI agents. Agents can estimate cost,
    trigger generation, and poll for results — all with structured JSON responses and no GUI required.

    **Authentication:** Bearer token (account-scoped API key) in the `Authorization` header.
    Example: `Authorization: Bearer sa_abc123...`

    **Contracts:** All request/response shapes are defined in `packages/types` (TypeScript).
    Fields use camelCase to match the canonical type definitions.
  version: 1.0.0
  contact:
    name: SorryAssets Team
  license:
    name: AGPL-3.0

servers:
  - url: https://api.sorryassets.com
    description: Production
  - url: http://localhost:8080
    description: Local development

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "sa_*"
      description: Account-scoped API key

  schemas:
    # Catalog
    CapabilityId:
      type: string
      enum:
        - image.generate
        - video.generate
      description: Capability identifier, typed by output modality.

    NodeType:
      type: string
      enum:
        - text
        - image
        - video
      description: Asset node type.

    ParamType:
      type: string
      enum:
        - enum
        - number
        - string
        - boolean
      description: Parameter type for generation.

    ParamSpec:
      type: object
      properties:
        key:
          type: string
          description: Parameter identifier (e.g., "duration", "resolution").
        type:
          $ref: "#/components/schemas/ParamType"
        values:
          type: array
          items:
            oneOf:
              - type: string
              - type: number
          description: Allowed values for enum parameters.
        unit:
          type: string
          description: Display unit hint (e.g., "s" for seconds).
        default:
          oneOf:
            - type: string
            - type: number
            - type: boolean
          description: Default value if not provided.
      required:
        - key
        - type

    InputSpec:
      type: object
      properties:
        role:
          type: string
          description: Role name (e.g., "prompt", "first_frame", "style_reference").
        type:
          $ref: "#/components/schemas/NodeType"
        min:
          type: integer
          minimum: 0
          description: Minimum required count (0 = optional).
        max:
          type: integer
          minimum: 1
          description: Maximum accepted count.
      required:
        - role
        - type
        - min
        - max

    ProviderBinding:
      type: object
      description: |
        One provider/model's binding to a capability.
        Declares the three-part signature: output + inputs (with roles) + params.
      properties:
        provider:
          type: string
          example: flux
          description: Provider identifier.
        model:
          type: string
          example: flux-1-dev
          description: Provider's model identifier.
        label:
          type: string
          example: Flux 1.0 (dev)
          description: Human-readable model name.
        inputs:
          type: array
          items:
            $ref: "#/components/schemas/InputSpec"
          description: Accepted input slots with roles.
        params:
          type: array
          items:
            $ref: "#/components/schemas/ParamSpec"
          description: Generation parameters this model accepts.
      required:
        - provider
        - model
        - label
        - inputs
        - params

    Capability:
      type: object
      description: A generation capability (e.g., image.generate).
      properties:
        id:
          $ref: "#/components/schemas/CapabilityId"
        label:
          type: string
          description: Result-oriented menu label, e.g. "Make image".
          example: Make image
        output:
          $ref: "#/components/schemas/NodeType"
          description: The produced node type.
        providers:
          type: array
          items:
            $ref: "#/components/schemas/ProviderBinding"
          description: Provider/model bindings for this capability.
      required:
        - id
        - label
        - output
        - providers

    Catalog:
      type: object
      description: Menu of available capabilities and provider bindings.
      properties:
        version:
          type: string
          example: "2026-06-28"
          description: Catalog version for caching/invalidation.
        capabilities:
          type: array
          items:
            $ref: "#/components/schemas/Capability"
      required:
        - version
        - capabilities

    # Generation
    RoleAssignment:
      type: object
      description: Maps a parent node to a role for a given provider.
      properties:
        nodeId:
          type: string
          example: node-123
        role:
          type: string
          example: prompt
      required:
        - nodeId
        - role

    GenerationParams:
      type: object
      description: |
        Serialized inputs and parameters for one generation.
        Stored on the edge; defines what was sent to the provider.
      properties:
        inputs:
          type: array
          items:
            $ref: "#/components/schemas/RoleAssignment"
          description: Role-tagged parent node references.
        values:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
          example:
            prompt: "a futuristic blue hero image, sci-fi, high-tech"
            size: "1024x768"
          description: Provider-declared parameters keyed by param key.
      required:
        - inputs
        - values

    TaskStatus:
      type: string
      enum:
        - queued
        - running
        - succeeded
        - failed
        - canceled
      description: Async task lifecycle state.

    # Billing
    Amount:
      type: number
      format: double
      example: 0.04
      description: Monetary amount in the `$`-scale display unit.

    CostEstimate:
      type: object
      description: Cost estimate before generation.
      properties:
        estimate:
          $ref: "#/components/schemas/Amount"
          description: Estimated charge.
        affordable:
          type: boolean
          example: true
          description: Whether current balance covers the estimate.
      required:
        - estimate
        - affordable

    # Requests
    EstimateRequest:
      type: object
      description: Request to estimate generation cost.
      properties:
        capability:
          $ref: "#/components/schemas/CapabilityId"
        provider:
          type: string
          example: flux
        model:
          type: string
          example: flux-1-dev
        params:
          $ref: "#/components/schemas/GenerationParams"
      required:
        - capability
        - provider
        - params

    GenerateRequest:
      type: object
      description: Request to start a generation.
      properties:
        projectId:
          type: string
          example: proj-abc123
          description: Project to write the local provenance graph into.
        capability:
          $ref: "#/components/schemas/CapabilityId"
        provider:
          type: string
          example: flux
        model:
          type: string
          example: flux-1-dev
        params:
          $ref: "#/components/schemas/GenerationParams"
        clientEdgeId:
          type: string
          example: edge-client-456
          description: Client-provided idempotency key for this edge.
      required:
        - projectId
        - capability
        - provider
        - params
        - clientEdgeId

    # Responses
    GenerateResponse:
      type: object
      description: Response from POST /v1/generate.
      properties:
        taskRef:
          type: string
          example: task-xyz789
          description: Backend task reference for polling.
        taskStatus:
          $ref: "#/components/schemas/TaskStatus"
      required:
        - taskRef
        - taskStatus

    TaskStatusResponse:
      type: object
      description: Response from GET /v1/task/:taskRef.
      properties:
        taskRef:
          type: string
          example: task-xyz789
        taskStatus:
          $ref: "#/components/schemas/TaskStatus"
        outputUrl:
          type: string
          format: uri
          example: https://sorryassets.com/v1/asset/sa-img-abc123
          description: Present when succeeded; where to fetch the output.
        cost:
          $ref: "#/components/schemas/Amount"
          description: Settled charge in `$`-scale; present on terminal states.
        error:
          type: string
          example: "insufficient balance"
          description: Human-readable reason when failed or canceled.
      required:
        - taskRef
        - taskStatus

    ApiError:
      type: object
      description: Standard error response for all endpoints.
      properties:
        code:
          type: string
          example: invalid_request
          enum:
            - invalid_request
            - method_not_allowed
            - unauthorized
            - insufficient_balance
            - submit_failed
            - poll_failed
            - task_not_found
            - internal_error
        message:
          type: string
          example: "capability is required"
      required:
        - code
        - message

paths:
  /v1/catalog:
    get:
      summary: "Get available capabilities and provider bindings"
      description: |
        Discover what can be generated, which providers/models support it,
        and what inputs + parameters each provider expects.

        Public (no auth): the catalog is pure discovery — branded labels, no
        secrets, no pricing — and the desktop's local-first menu needs it
        without a session. Pricing is never included; cost comes from
        `POST /v1/estimate`, which returns the flat price for the chosen
        public model.

        Agents use this to dynamically discover capabilities instead of
        baking provider/model lists into their prompt. Adding a provider is a
        backend change only — no client release.
      operationId: getCatalog
      responses:
        "200":
          description: Catalog retrieved successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Catalog"
              example:
                version: "2026-06-28"
                capabilities:
                  - id: image.generate
                    output: image
                    providers:
                      - provider: flux
                        model: flux-1-dev
                        label: Flux 1.0 (dev)
                        inputs:
                          - role: prompt
                            type: text
                            min: 1
                            max: 1
                        params:
                          - key: prompt
                            type: string
                          - key: size
                            type: enum
                            values: ["1024x768", "1024x1024", "1792x1024"]
                      - provider: dall-e
                        model: dall-e-3
                        label: DALL-E 3
                        inputs:
                          - role: prompt
                            type: text
                            min: 1
                            max: 1
                        params:
                          - key: prompt
                            type: string
                          - key: size
                            type: enum
                            values: ["1024x1024"]
                  - id: video.generate
                    output: video
                    providers: []
        "401":
          description: Unauthorized (invalid or missing API key)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/estimate:
    post:
      summary: Estimate generation cost
      description: |
        Get a cost estimate before generating. Use this to show the user
        (or agent) how much a generation will cost, and whether current
        balance covers it. The estimate is binding: if you immediately
        call /v1/generate with the same params, the charge will match
        (barring rate/quota changes on the provider side).
      operationId: estimate
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EstimateRequest"
            example:
              capability: image.generate
              provider: flux
              model: flux-1-dev
              params:
                inputs: []
                values:
                  prompt: "a futuristic blue hero image, sci-fi, high-tech, wide banner"
                  size: "1024x768"
      responses:
        "200":
          description: Cost estimate retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CostEstimate"
              example:
                estimate: 0.04
                affordable: true
        "400":
          description: Invalid request (missing/invalid fields)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/generate:
    post:
      summary: Start a generation
      description: |
        Submit a generation request. The backend pre-charges (holds) the
        estimated balance, calls the provider, and returns a task reference
        to poll.

        The client should write a local edge in "queued" state immediately,
        then poll GET /v1/task/:taskRef until terminal state.

        Idempotency: if you retry this request with the same `clientEdgeId`,
        the backend deduplicates and returns the existing `taskRef`.
      operationId: generate
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateRequest"
            example:
              projectId: proj-abc123
              capability: image.generate
              provider: flux
              model: flux-1-dev
              params:
                inputs: []
                values:
                  prompt: "a futuristic blue hero image, sci-fi, high-tech, wide banner"
                  size: "1024x768"
              clientEdgeId: edge-client-456
      responses:
        "200":
          description: Generation started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenerateResponse"
              example:
                taskRef: task-xyz789
                taskStatus: queued
        "400":
          description: Invalid request (missing fields, unsupported capability, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              example:
                code: invalid_request
                message: "capability is required"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "402":
          description: Insufficient balance (insufficient_balance code)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              example:
                code: insufficient_balance
                message: "balance insufficient for estimated cost"
        "500":
          description: Internal error (submit_failed, poll_failed)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v1/task/{taskRef}:
    get:
      summary: Poll generation status
      description: |
        Poll the status of a generation task. Initially returns "queued" or "running".
        Terminal states are "succeeded", "failed", or "canceled".

        When succeeded, `outputUrl` and `cost` are populated.
        When failed/canceled, `error` is populated.

        Agents should poll this endpoint every 1–5 seconds until terminal state.
        The backend's typical generation latency is 5–120 seconds depending on provider.
      operationId: getTaskStatus
      security:
        - bearerAuth: []
      parameters:
        - name: taskRef
          in: path
          required: true
          description: Task reference returned by POST /v1/generate
          schema:
            type: string
            example: task-xyz789
      responses:
        "200":
          description: Task status retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskStatusResponse"
              examples:
                queued:
                  value:
                    taskRef: task-xyz789
                    taskStatus: queued
                running:
                  value:
                    taskRef: task-xyz789
                    taskStatus: running
                succeeded:
                  value:
                    taskRef: task-xyz789
                    taskStatus: succeeded
                    outputUrl: "https://sorryassets.com/v1/asset/sa-img-abc123"
                    cost: 0.04
                failed:
                  value:
                    taskRef: task-xyz789
                    taskStatus: failed
                    error: "provider returned error: unsupported parameters"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "404":
          description: Task not found (task_not_found code)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              example:
                code: task_not_found
                message: "task does not exist"
        "500":
          description: Internal error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

security:
  - bearerAuth: []

tags:
  - name: Catalog
    description: Discover capabilities and provider bindings
  - name: Generation
    description: Estimate, start, and poll generations
