openapi: 3.1.0

info:
  title: GetRun API
  version: 1.0.0-draft
  description: |
    API publik **GetRun by Getcore** — jalankan model generative AI (text, image,
    video, audio) dengan satu API key dan satu saldo.

    Base URL: `https://run.getcore.id/v1`

    ## Pola umum
    - Semua request memakai `Authorization: Bearer run_live_...`.
    - Generation berjalan **asynchronous**: buat task → status `queued` →
      `processing` → `succeeded`/`failed`. Hasil diambil via polling
      (`GET /tasks/{task_id}`) atau webhook.
    - **LLM adalah pengecualian**: `POST /chat/completions` bersifat synchronous
      (dengan opsi streaming SSE) dan kompatibel dengan SDK OpenAI.
    - Semua nominal uang adalah **integer Rupiah (IDR)** — tidak ada desimal.
    - Task yang gagal tanpa output valid **tidak ditagih**.
    - Semua endpoint mutasi mendukung header `Idempotency-Key`; wajib untuk
      `POST /tasks`. Request ulang dengan key yang sama mengembalikan hasil
      yang sama tanpa membuat task baru.
    - Setiap response membawa header `X-Request-Id` untuk penelusuran.
  contact:
    email: support@getcore.id

servers:
  - url: https://run.getcore.id/v1

security:
  - bearerAuth: []

tags:
  - name: Models
    description: Katalog model
  - name: Quotes
    description: Estimasi harga sebelum membuat task
  - name: Tasks
    description: Lifecycle generation task
  - name: Files
    description: Upload file input
  - name: Billing
    description: Saldo dan penggunaan
  - name: LLM
    description: Jalur OpenAI-compatible (synchronous + streaming)

paths:
  /models:
    get:
      operationId: listModels
      tags: [Models]
      summary: Daftar model
      description: >
        Mengembalikan katalog model. Hanya model dengan availability selain
        `offline` yang muncul secara default.
      parameters:
        - name: category
          in: query
          schema:
            $ref: '#/components/schemas/ModelCategory'
        - name: availability
          in: query
          schema:
            $ref: '#/components/schemas/ModelAvailability'
        - name: search
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Daftar model.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Model' }
        default: { $ref: '#/components/responses/Error' }

  /models/{creator}/{slug}:
    get:
      operationId: getModel
      tags: [Models]
      summary: Detail model
      description: >
        `model_id` publik berbentuk `{creator}/{slug}`, misalnya
        `google/veo-3.1`. ID ini stabil — tidak berubah walau upstream
        provider diganti.
      parameters:
        - name: creator
          in: path
          required: true
          schema: { type: string }
          example: google
        - name: slug
          in: path
          required: true
          schema: { type: string }
          example: veo-3.1
      responses:
        '200':
          description: Detail model.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Model' }
        default: { $ref: '#/components/responses/Error' }

  /uploads:
    post:
      operationId: uploadInput
      tags: [Uploads]
      summary: Upload file input
      description: >
        Upload file untuk parameter model bertipe file (image_urls,
        first_frame_url, reference_video_urls, video_list, dsb). Kirim
        multipart/form-data dengan field `file` — bukan JSON/base64.
        Gambar/audio maksimum 25 MB, video 50 MB; file disimpan 7 hari.
        Pakai `url` dari respons sebagai nilai parameter file di task.
        Video referensi WAJIB dari endpoint ini — durasinya diukur saat
        upload dan menjadi dasar harga per detik (URL video eksternal
        ditolak 400).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: File tersimpan.
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string }
                  type: { type: string, enum: [image, video, audio] }
                  size: { type: integer }
                  duration_seconds:
                    type: number
                    nullable: true
                    description: Durasi video/audio (detik); null untuk gambar.
        default: { $ref: '#/components/responses/Error' }

  /quotes:
    post:
      operationId: createQuote
      tags: [Quotes]
      summary: Buat price quote
      description: >
        Menghitung estimasi biaya untuk kombinasi model + parameter tanpa
        menjalankan task. Quote berlaku 10 menit. Membuat task tidak wajib
        didahului quote — `POST /tasks` menghitung harga sendiri bila
        `quote_id` tidak diberikan.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, input]
              properties:
                model:
                  type: string
                  example: google/veo-3.1
                input:
                  type: object
                  description: Parameter yang memengaruhi harga.
                  additionalProperties: true
      responses:
        '201':
          description: Quote dibuat.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Quote' }
        default: { $ref: '#/components/responses/Error' }

  /tasks:
    post:
      operationId: createTask
      tags: [Tasks]
      summary: Buat generation task
      description: |
        Memvalidasi input terhadap parameter schema model, menyimpan price
        snapshot, mereservasi saldo maksimum, lalu mengantrekan task.

        Task gagal tidak ditagih; reservasi dilepas otomatis.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, input]
              properties:
                model:
                  type: string
                  example: google/veo-3.1
                input:
                  type: object
                  description: >
                    Parameter model sesuai `parameters_schema` pada detail model.
                  additionalProperties: true
                  example:
                    prompt: A cinematic sunrise above Jakarta
                    duration_seconds: 8
                    aspect_ratio: '16:9'
                quote_id:
                  type: string
                  description: >
                    Opsional. Bila diberikan dan masih berlaku, harga quote
                    dipakai sebagai snapshot.
                webhook_url:
                  type: string
                  format: uri
                  description: >
                    Opsional; menimpa webhook default project untuk task ini.
                metadata:
                  type: object
                  additionalProperties: { type: string }
                  description: Key-value bebas milik client, dikembalikan apa adanya.
      responses:
        '201':
          description: Task diterima dan berstatus `queued`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Task' }
        default: { $ref: '#/components/responses/Error' }

  /tasks/{task_id}:
    get:
      operationId: getTask
      tags: [Tasks]
      summary: Status dan hasil task
      parameters:
        - $ref: '#/components/parameters/TaskId'
      responses:
        '200':
          description: Task.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Task' }
        default: { $ref: '#/components/responses/Error' }

  /tasks/{task_id}/cancel:
    post:
      operationId: cancelTask
      tags: [Tasks]
      summary: Batalkan task
      description: >
        Hanya berhasil bila task belum selesai dan upstream mendukung
        pembatalan; selain itu mengembalikan error `task_not_cancellable`.
      parameters:
        - $ref: '#/components/parameters/TaskId'
      responses:
        '200':
          description: Task dibatalkan.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Task' }
        default: { $ref: '#/components/responses/Error' }

  /files:
    post:
      operationId: uploadFile
      tags: [Files]
      summary: Upload file input
      description: >
        Mengunggah file (gambar referensi, audio, dsb.) untuk dipakai sebagai
        input task. File disimpan 7 hari.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: File tersimpan.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FileObject' }
        default: { $ref: '#/components/responses/Error' }

  /balance:
    get:
      operationId: getBalance
      tags: [Billing]
      summary: Saldo saat ini
      responses:
        '200':
          description: Saldo.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Balance' }
        default: { $ref: '#/components/responses/Error' }

  /usage:
    get:
      operationId: listUsage
      tags: [Billing]
      summary: Riwayat penggunaan
      description: Daftar penggunaan berbayar (task dan LLM), terbaru lebih dulu.
      parameters:
        - name: starting_after
          in: query
          description: Cursor pagination — id item terakhir dari halaman sebelumnya.
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: Halaman penggunaan.
          content:
            application/json:
              schema:
                type: object
                required: [data, has_more]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/UsageItem' }
                  has_more: { type: boolean }
        default: { $ref: '#/components/responses/Error' }

  /chat/completions:
    post:
      operationId: createChatCompletion
      tags: [LLM]
      summary: Chat completion (OpenAI-compatible)
      description: |
        Jalur LLM synchronous, kompatibel dengan SDK OpenAI — arahkan
        `base_url` SDK ke `https://run.getcore.id/v1`.

        Billing per token: saldo direservasi dari `max_tokens`, di-capture
        berdasarkan token aktual. Dengan `stream: true`, response berupa
        Server-Sent Events dan `usage` dikirim pada chunk terakhir.

        Endpoint ini tidak memakai lifecycle task dan tidak memakai webhook.
        Error mengikuti format OpenAI (`{"error": {"message", "type", "code"}}`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, messages]
              properties:
                model:
                  type: string
                  example: anthropic/claude-sonnet-5
                messages:
                  type: array
                  items:
                    type: object
                    required: [role, content]
                    properties:
                      role:
                        type: string
                        enum: [system, user, assistant, tool]
                      content: {}
                stream: { type: boolean, default: false }
                max_tokens: { type: integer, minimum: 1 }
                temperature: { type: number }
              additionalProperties: true
      responses:
        '200':
          description: >
            Chat completion (JSON), atau stream SSE bila `stream: true`.
          content:
            application/json:
              schema:
                type: object
                description: Objek `chat.completion` sesuai format OpenAI.
                additionalProperties: true
            text/event-stream:
              schema:
                type: string
                description: 'Stream chunk `chat.completion.chunk`, diakhiri `data: [DONE]`.'
        default:
          description: Error berformat OpenAI-compatible.
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error:
                    type: object
                    properties:
                      message: { type: string }
                      type: { type: string }
                      code: { type: string }

webhooks:
  taskEvent:
    post:
      operationId: taskEventWebhook
      summary: Task event webhook
      description: |
        Dikirim ke `webhook_url` (per task, atau default project) pada setiap
        perubahan status: `task.processing`, `task.succeeded`, `task.failed`,
        `task.cancelled`.

        - Signature: header `X-Run-Signature` berformat `t=<unix_ts>,v1=<hex>`
          — HMAC SHA-256 atas `"{t}.{raw_body}"` dengan signing secret project.
          Tolak bila selisih `t` dengan waktu sekarang melebihi toleransi
          (rekomendasi 5 menit).
        - Balas HTTP 2xx untuk menandai sukses; selain itu dikirim ulang
          dengan exponential backoff.
        - Delivery duplikat mungkin terjadi — gunakan `id` event untuk dedup.
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200':
          description: Diterima client.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'API key GetRun, format: `run_live_...`'

  parameters:
    TaskId:
      name: task_id
      in: path
      required: true
      schema: { type: string }
      example: task_01J8Z3N0V9X2M4K6P8Q0R2S4T6
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      description: >
        Key unik per operasi (maks 255 karakter, disimpan 24 jam). Request
        ulang dengan key sama mengembalikan response asli tanpa efek ganda.
      schema: { type: string, maxLength: 255 }
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }

  responses:
    Error:
      description: Error envelope standar.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  schemas:
    ModelCategory:
      type: string
      enum: [text, image, video, audio, utility]

    ModelAvailability:
      type: string
      enum: [online, degraded, maintenance, offline, deprecated]

    Model:
      type: object
      required: [model_id, display_name, creator, category, availability]
      properties:
        model_id:
          type: string
          description: ID publik stabil, format `{creator}/{slug}`.
          example: google/veo-3.1
        display_name: { type: string, example: 'Veo 3.1' }
        creator: { type: string, example: google }
        category: { $ref: '#/components/schemas/ModelCategory' }
        availability: { $ref: '#/components/schemas/ModelAvailability' }
        input_modes:
          type: array
          items:
            type: string
            enum: [text, image, video, audio]
        output_type:
          type: string
          example: video/mp4
        parameters_schema:
          type: object
          description: JSON Schema parameter input model.
          additionalProperties: true
        pricing:
          type: object
          properties:
            display:
              type: string
              description: Formula harga dalam bentuk terbaca.
              example: 'Rp2.812 / detik video'
            example_idr:
              type: integer
              description: Contoh harga untuk konfigurasi umum, dalam IDR.
              example: 22500
        estimated_time_seconds:
          type: object
          description: Rentang perkiraan, bukan janji.
          properties:
            min: { type: integer }
            max: { type: integer }
        version: { type: string }
        trial_eligible:
          type: boolean
          description: Apakah model dapat dipakai dengan trial credit.

    Quote:
      type: object
      required: [id, model, currency, estimated, max_reservable, pricing_version, expires_at]
      properties:
        id: { type: string, example: quote_01J8Z3N0V9X2M4K6P8Q0R2S4T6 }
        model: { type: string, example: google/veo-3.1 }
        currency: { type: string, const: IDR }
        estimated:
          type: integer
          description: Perkiraan biaya (IDR).
          example: 22500
        max_reservable:
          type: integer
          description: Jumlah maksimum yang akan direservasi (IDR).
          example: 22500
        pricing_version: { type: string, example: 'google/veo-3.1@3' }
        expires_at: { type: string, format: date-time }
        note:
          type: string
          description: Diisi bila biaya final bergantung pada output aktual.

    TaskStatus:
      type: string
      enum: [queued, processing, succeeded, failed, cancelled]

    Task:
      type: object
      required: [id, status, model, price, created_at]
      properties:
        id: { type: string, example: task_01J8Z3N0V9X2M4K6P8Q0R2S4T6 }
        status: { $ref: '#/components/schemas/TaskStatus' }
        model: { type: string, example: google/veo-3.1 }
        input:
          type: object
          additionalProperties: true
        price:
          type: object
          required: [currency, estimated, reserved]
          properties:
            currency: { type: string, const: IDR }
            estimated: { type: integer, example: 22500 }
            reserved: { type: integer, example: 22500 }
            final:
              type: integer
              description: Biaya final; hadir setelah task selesai. 0 bila gagal.
            pricing_version: { type: string }
        output:
          type: object
          description: Hadir bila `status = succeeded`.
          properties:
            urls:
              type: array
              items: { type: string, format: uri }
            expires_at:
              type: string
              format: date-time
              description: Output dihapus setelah 7 hari — salin ke storage sendiri.
        error:
          type: object
          description: Hadir bila `status = failed`.
          properties:
            code: { $ref: '#/components/schemas/ErrorCode' }
            message: { type: string }
        webhook_url: { type: string, format: uri }
        metadata:
          type: object
          additionalProperties: { type: string }
        created_at: { type: string, format: date-time }
        started_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }

    FileObject:
      type: object
      required: [id, url, content_type, size_bytes, expires_at]
      properties:
        id: { type: string, example: file_01J8Z3N0V9X2M4K6P8Q0R2S4T6 }
        url:
          type: string
          format: uri
          description: URL managed storage — pakai ini sebagai nilai input task.
        content_type: { type: string, example: image/png }
        size_bytes: { type: integer }
        expires_at: { type: string, format: date-time }

    Balance:
      type: object
      required: [currency, available, reserved]
      properties:
        currency: { type: string, const: IDR }
        available:
          type: integer
          description: Saldo yang dapat dipakai (IDR).
          example: 30000
        reserved:
          type: integer
          description: Sedang dikunci untuk task berjalan (IDR).
          example: 22500

    UsageItem:
      type: object
      required: [id, kind, amount, currency, created_at]
      properties:
        id: { type: string }
        kind:
          type: string
          enum: [task, llm]
        task_id: { type: string }
        model: { type: string }
        amount:
          type: integer
          description: Biaya final (IDR).
        currency: { type: string, const: IDR }
        tokens:
          type: object
          description: Hanya untuk `kind = llm`.
          properties:
            input: { type: integer }
            output: { type: integer }
        created_at: { type: string, format: date-time }

    WebhookEvent:
      type: object
      required: [id, type, created_at, attempt, data]
      properties:
        id: { type: string, example: evt_01J8Z3N0V9X2M4K6P8Q0R2S4T6 }
        type:
          type: string
          enum: [task.processing, task.succeeded, task.failed, task.cancelled]
        created_at: { type: string, format: date-time }
        attempt:
          type: integer
          description: Nomor attempt delivery, mulai dari 1.
        data:
          $ref: '#/components/schemas/Task'

    ErrorCode:
      type: string
      description: Kode error ternormalisasi — status internal upstream tidak pernah bocor.
      enum:
        - invalid_request
        - unauthorized
        - forbidden
        - not_found
        - idempotency_conflict
        - rate_limited
        - insufficient_balance
        - quote_expired
        - model_not_found
        - model_offline
        - unsupported_parameter
        - moderation_blocked
        - upstream_error
        - upstream_timeout
        - task_not_cancellable
        - internal_error

    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { $ref: '#/components/schemas/ErrorCode' }
            message: { type: string, example: Balance is lower than the maximum task cost. }
            request_id: { type: string, example: req_01J8Z3N0V9X2M4K6P8Q0R2S4T6 }
            details:
              type: object
              additionalProperties: true
