{
  "openapi": "3.1.0",
  "info": {
    "title": "Wave API",
    "version": "1.0.0",
    "description": "API thống nhất của Wave để tạo nhạc, ảnh và video. Mọi route dùng khóa API dạng `Authorization: Bearer sk_live_...`, trừ `/files/{id}` và `/api/v1/media/{id}` (id chính là quyền truy cập). Base URL là origin đang phục vụ tài liệu này (`/openapi.json`). Lỗi trả về dạng `{\"error\":{\"code\",\"message\"}}`; riêng facade OpenAI (`/api/v1/images/generations`, `/edits`) dùng envelope `{\"error\":{\"message\",\"type\",\"code\",\"param\"}}`."
  },
  "servers": [
    {
      "url": "/",
      "description": "Cùng origin với trang này: host đang phục vụ /openapi.json và /docs/."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "account",
      "description": "Thông tin khóa API"
    },
    {
      "name": "jobs",
      "description": "Gửi và theo dõi job"
    },
    {
      "name": "models",
      "description": "Danh sách model và giá"
    },
    {
      "name": "files",
      "description": "Kết quả và media"
    },
    {
      "name": "openai",
      "description": "Facade tương thích OpenAI Images"
    }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "account"
        ],
        "summary": "Thông tin khóa API hiện tại",
        "description": "Trả về khách hàng, khóa, trạng thái, giới hạn tốc độ và phạm vi (scope) của khóa.",
        "responses": {
          "200": {
            "description": "Principal của khóa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/music/jobs": {
      "post": {
        "operationId": "submitMusicJob",
        "tags": [
          "jobs"
        ],
        "summary": "Gửi job nhạc",
        "description": "Kind mặc định là `music.generate`; các kind `music.*` khác (và `fake.echo` khi bật provider fake) chọn qua trường `kind`. Body JSON tối đa 1 MiB. Multipart: part text là từng field JSON (true/false/số/null/`{`/`[` được ép kiểu, còn lại là chuỗi), hoặc một part `request` chứa cả object; part file `audio` (tối đa 20 MiB mỗi file, mashup tối đa 2 file) là nguồn âm thanh.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MusicJobRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/MusicMultipartRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/JobAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/images/jobs": {
      "post": {
        "operationId": "submitImageJob",
        "tags": [
          "jobs"
        ],
        "summary": "Gửi job ảnh",
        "description": "Kind mặc định là `image.generate`; `image.edit` chọn qua trường `kind`. Provider do `model` quyết định (chatgpt: `gpt-image-1`; flow: `nano-banana-2-lite`, `nano-banana-2`, `nano-banana-pro`; grok: `grok-imagine`, `grok-imagine-pro`; bỏ trống/`auto` để Wave chọn). Field provider không hỗ trợ bị từ chối (400) trừ khi `drop_unsupported: true`. Multipart: part file `image` (nguồn của image.edit, đúng một) và `reference` (ảnh tham chiếu, flow).",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImageJobRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ImageMultipartRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/JobAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/videos/jobs": {
      "post": {
        "operationId": "submitVideoJob",
        "tags": [
          "jobs"
        ],
        "summary": "Gửi job video",
        "description": "Kind duy nhất là `video.generate`. Provider do `model` quyết định (flow: `veo-3.1-lite`, `veo-3.1-fast`, `veo-3.1-quality`, `omni-flash`; grok: `grok-imagine-video`; bỏ trống/`auto` để Wave chọn). Chế độ suy ra từ nguồn: không ảnh → text-to-video, `start_image` → image-to-video, `start_image`+`end_image` → nội suy (flow), `references` → ingredients (flow). Multipart: part file `start_image`, `end_image`, `reference`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VideoGenerateRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/VideoMultipartRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/JobAccepted"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/music/models": {
      "get": {
        "operationId": "listMusicModels",
        "tags": [
          "models"
        ],
        "summary": "Model nhạc đang có giá",
        "description": "Mọi cặp (kind, model) của medium music có giá và có provider; `enabled` cho biết hiện có tài khoản phục vụ được không.",
        "responses": {
          "200": {
            "$ref": "#/components/responses/ModelList"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/images/models": {
      "get": {
        "operationId": "listImageModels",
        "tags": [
          "models"
        ],
        "summary": "Model ảnh đang có giá",
        "responses": {
          "200": {
            "$ref": "#/components/responses/ModelList"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/videos/models": {
      "get": {
        "operationId": "listVideoModels",
        "tags": [
          "models"
        ],
        "summary": "Model video đang có giá",
        "responses": {
          "200": {
            "$ref": "#/components/responses/ModelList"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/jobs": {
      "get": {
        "operationId": "listJobs",
        "tags": [
          "jobs"
        ],
        "summary": "Liệt kê job",
        "description": "Job của khách hàng, mới nhất trước. Phân trang bằng `cursor` trả về trong response; vắng `cursor` nghĩa là hết.",
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "description": "Lọc theo kind.",
            "schema": {
              "$ref": "#/components/schemas/JobKind"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Lọc theo trạng thái kết thúc.",
            "schema": {
              "type": "string",
              "enum": [
                "succeeded",
                "failed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Giá trị `cursor` của trang trước (chuỗi mờ).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Trang job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/jobs/{id}": {
      "get": {
        "operationId": "getJob",
        "tags": [
          "jobs"
        ],
        "summary": "Xem một job",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/files/{id}": {
      "get": {
        "operationId": "getFile",
        "tags": [
          "files"
        ],
        "summary": "Tải kết quả",
        "description": "Không cần khóa API: id 128-bit là quyền truy cập. Chuyển hướng 302 tới URL ký ngắn hạn khi kết quả còn, 410 `EXPIRED` khi hết hạn, 404 khi không có.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Chuyển hướng tới URL tải (Cache-Control: private, max-age=60).",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "410": {
            "$ref": "#/components/responses/Gone"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/media/{id}": {
      "get": {
        "operationId": "streamMedia",
        "tags": [
          "files"
        ],
        "summary": "Phát audio đang tạo",
        "description": "Không cần khóa API; id là object upstream trong `stream_url` của item preview. Hỗ trợ Range dạng `bytes=a-` và `bytes=a-b` (206); dạng khác trả cả file (200). Khi rendition không seek được, Range bị bỏ qua.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "mp3",
                "m4a"
              ],
              "default": "mp3"
            }
          },
          {
            "name": "Range",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "example": "bytes=0-"
          }
        ],
        "responses": {
          "200": {
            "description": "Cả file audio.",
            "headers": {
              "Content-Type": {
                "schema": {
                  "type": "string"
                }
              },
              "Accept-Ranges": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "audio/mp4": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "206": {
            "description": "Một phần file theo Range.",
            "headers": {
              "Content-Range": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Length": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "audio/mp4": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "416": {
            "$ref": "#/components/responses/RangeNotSatisfiable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/media/{id}/lyrics": {
      "get": {
        "operationId": "getMediaLyrics",
        "tags": [
          "files"
        ],
        "summary": "Lời bài hát có thời gian",
        "description": "Không cần khóa API; cùng mô hình quyền với `/api/v1/media/{id}`.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Từng từ và mốc thời gian (Cache-Control: private, max-age=3600).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlignedLyrics"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/v1/images/generations": {
      "post": {
        "operationId": "openaiImageGenerations",
        "tags": [
          "openai"
        ],
        "summary": "Tạo ảnh (OpenAI facade)",
        "description": "Chạy đồng bộ một job `image.generate` và trả về dạng OpenAI; SDK OpenAI trỏ `base_url=<wave>/api/v1` dùng được ngay. Chờ tối đa `server.direct_timeout` (mặc định 10 phút); hết giờ trả 504 kèm `job_id` để lấy kết quả qua `GET /api/v1/jobs/{id}`. Field lạ → 400; `stream:true` → 400.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenAIImageGenerationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ảnh đã tạo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIImagesResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/OpenAIBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/OpenAIUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/OpenAIInsufficientQuota"
          },
          "403": {
            "$ref": "#/components/responses/OpenAIForbidden"
          },
          "404": {
            "$ref": "#/components/responses/OpenAINotFound"
          },
          "429": {
            "$ref": "#/components/responses/OpenAIRateLimit"
          },
          "502": {
            "$ref": "#/components/responses/OpenAIServerError"
          },
          "504": {
            "$ref": "#/components/responses/OpenAITimeout"
          }
        }
      }
    },
    "/api/v1/images/edits": {
      "post": {
        "operationId": "openaiImageEdits",
        "tags": [
          "openai"
        ],
        "summary": "Sửa ảnh (OpenAI facade)",
        "description": "Multipart với đúng một file `image` và `prompt`; chạy đồng bộ một job `image.edit`. `n` phải là 1; `mask` không hỗ trợ (400); `stream=true` → 400. Hết giờ chờ trả 504 kèm `job_id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/OpenAIImageEditRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ảnh đã sửa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIImagesResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/OpenAIBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/OpenAIUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/OpenAIInsufficientQuota"
          },
          "403": {
            "$ref": "#/components/responses/OpenAIForbidden"
          },
          "404": {
            "$ref": "#/components/responses/OpenAINotFound"
          },
          "429": {
            "$ref": "#/components/responses/OpenAIRateLimit"
          },
          "502": {
            "$ref": "#/components/responses/OpenAIServerError"
          },
          "504": {
            "$ref": "#/components/responses/OpenAITimeout"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Khóa API `sk_live_...` trong header `Authorization: Bearer <key>`."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Trở thành `request_id` của job (tối đa 200 ký tự). Gửi lại cùng khóa với cùng body trả về job cũ; khác body → 409 `IDEMPOTENCY_MISMATCH`. Bỏ trống thì Wave tự sinh.",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      }
    },
    "responses": {
      "JobAccepted": {
        "description": "Job đã được nhận và xếp hàng.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Job"
            }
          }
        }
      },
      "ModelList": {
        "description": "Danh sách model.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "models"
              ],
              "properties": {
                "models": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ModelEntry"
                  }
                }
              }
            }
          }
        }
      },
      "BadRequest": {
        "description": "Yêu cầu không hợp lệ: `INVALID_REQUEST`, `INVALID_INPUT`, `NO_PRICE`, `MODEL_UNAVAILABLE`, `SENSITIVE_WORD_ERROR`, `ADMISSION_INVALID`, `UNKNOWN_SOURCE_CLIP`, `AFFINITY_MISMATCH`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Thiếu hoặc sai khóa API (`UNAUTHORIZED`).",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            },
            "example": "Bearer realm=\"wave\""
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Số dư không đủ (`INSUFFICIENT_BALANCE`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Khóa không được phép (`SCOPE_DENIED`), tài khoản bị khóa (`CUSTOMER_SUSPENDED`) hoặc chưa xác minh email (`EMAIL_UNVERIFIED`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Không tìm thấy (`NOT_FOUND`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Xung đột: `IDEMPOTENCY_MISMATCH` hoặc `CONFLICT` (audio chưa sẵn sàng).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Gone": {
        "description": "Kết quả đã hết hạn (`EXPIRED`).",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "EXPIRED",
                "message": "this file has expired"
              }
            }
          }
        }
      },
      "RangeNotSatisfiable": {
        "description": "Range nằm ngoài kích thước file; không có body.",
        "headers": {
          "Content-Range": {
            "schema": {
              "type": "string"
            },
            "example": "bytes */1234567"
          }
        }
      },
      "TooManyRequests": {
        "description": "Vượt giới hạn tốc độ của khóa (`RATE_LIMITED`).",
        "headers": {
          "Retry-After": {
            "description": "Số giây nên chờ.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadGateway": {
        "description": "Upstream không phục vụ được (`UPSTREAM_UNAVAILABLE`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "Lỗi nội bộ (`INTERNAL`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "OpenAIBadRequest": {
        "description": "`invalid_request_error`: body/field sai (`param` chỉ field), `content_policy_violation`, `upstream_rejected`, `invalid_input`, `model_unavailable`, `no_price`, `idempotency_mismatch`, `unknown_source_clip`, `affinity_mismatch`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      },
      "OpenAIUnauthorized": {
        "description": "`invalid_api_key`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      },
      "OpenAIInsufficientQuota": {
        "description": "`insufficient_quota`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      },
      "OpenAIForbidden": {
        "description": "`scope_denied` (scope, tài khoản bị khóa, email chưa xác minh).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      },
      "OpenAINotFound": {
        "description": "`not_found`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      },
      "OpenAIRateLimit": {
        "description": "`rate_limit_error` / `rate_limit_exceeded` (giới hạn khóa, backlog hoặc upstream).",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      },
      "OpenAIServerError": {
        "description": "`server_error`: job thất bại (code là mã lỗi job viết thường, kèm `job_id`) hoặc `missing_image`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      },
      "OpenAITimeout": {
        "description": "`server_error` / `timeout`: hết giờ chờ, job vẫn chạy; `job_id` để poll `GET /api/v1/jobs/{id}`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OpenAIError"
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "description": "Mã lỗi Wave. Mã trả ngay khi gọi API: INVALID_REQUEST, NOT_FOUND, UNAUTHORIZED, SCOPE_DENIED, CUSTOMER_SUSPENDED, EMAIL_UNVERIFIED, RATE_LIMITED, NO_PRICE, INSUFFICIENT_BALANCE, IDEMPOTENCY_MISMATCH, BACKLOG, CONFLICT, INTERNAL, UPSTREAM_UNAVAILABLE, MODEL_UNAVAILABLE, ADMISSION_INVALID, UNKNOWN_SOURCE_CLIP, AFFINITY_MISMATCH, INVALID_INPUT. Các mã còn lại là lỗi kết thúc của job (`Job.error.code`). `EXPIRED` chỉ có ở `GET /files/{id}`.",
        "enum": [
          "INVALID_REQUEST",
          "NOT_FOUND",
          "UNAUTHORIZED",
          "SCOPE_DENIED",
          "CUSTOMER_SUSPENDED",
          "EMAIL_UNVERIFIED",
          "RATE_LIMITED",
          "NO_PRICE",
          "INSUFFICIENT_BALANCE",
          "IDEMPOTENCY_MISMATCH",
          "BACKLOG",
          "CONFLICT",
          "INTERNAL",
          "QUEUE_TIMEOUT",
          "PROCESSING_TIMEOUT",
          "AFFINITY_DEAD",
          "ADMISSION_INVALID",
          "MODEL_UNAVAILABLE",
          "SUBMISSION_UNKNOWN",
          "NOTHING_DELIVERED",
          "CAPTCHA_CAPACITY",
          "CAPTCHA_SOLVE_FAILED",
          "CAPTCHA_UNAVAILABLE",
          "CAPTCHA_TOKEN_REJECTED",
          "POOL_EXHAUSTED",
          "ACCOUNT_AUTH",
          "UPSTREAM_REJECTED",
          "UPSTREAM_UNAVAILABLE",
          "UPSTREAM_RATE_LIMIT",
          "EMPTY_UPSTREAM_RESULT",
          "GENERATION_FAILED",
          "GENERATION_TIMEOUT",
          "UNKNOWN_SOURCE_CLIP",
          "AFFINITY_MISMATCH",
          "INVALID_INPUT",
          "SENSITIVE_WORD_ERROR",
          "UPLOAD_FAILED",
          "EXPIRED"
        ]
      },
      "Error": {
        "type": "object",
        "description": "Envelope lỗi chung.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "OpenAIError": {
        "type": "object",
        "description": "Envelope lỗi của facade OpenAI.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "message",
              "type"
            ],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "invalid_request_error",
                  "insufficient_quota",
                  "rate_limit_error",
                  "server_error"
                ]
              },
              "code": {
                "type": "string",
                "description": "`invalid_api_key`, `insufficient_quota`, `rate_limit_exceeded`, `scope_denied`, `content_policy_violation`, `upstream_rejected`, `not_found`, `timeout`, `missing_image`, hoặc mã lỗi Wave viết thường (`invalid_request`, `model_unavailable`, ...)."
              },
              "param": {
                "type": "string",
                "description": "Field gây lỗi khi là lỗi validate của facade."
              },
              "job_id": {
                "type": "string",
                "description": "Mở rộng của Wave: id job khi job đã được tạo (504 hoặc job thất bại)."
              }
            }
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "customer_id",
          "api_key_id",
          "customer_status",
          "rate_limit_per_minute",
          "scope"
        ],
        "properties": {
          "customer_id": {
            "type": "string"
          },
          "api_key_id": {
            "type": "string"
          },
          "customer_status": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "closed"
            ]
          },
          "rate_limit_per_minute": {
            "type": "integer"
          },
          "scope": {
            "description": "Phạm vi của khóa; null hoặc mảng rỗng = không giới hạn ở trục đó.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "properties": {
                  "kinds": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/JobKind"
                    }
                  },
                  "models": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            ]
          }
        }
      },
      "JobKind": {
        "type": "string",
        "enum": [
          "music.generate",
          "music.extend",
          "music.cover",
          "music.add_vocals",
          "music.add_instrumental",
          "music.upload_cover",
          "music.upload_extend",
          "music.mashup",
          "music.infill",
          "music.lyrics",
          "music.persona",
          "music.cover_art",
          "music.vocal_removal",
          "music.midi",
          "music.wav",
          "music.mp4",
          "music.sounds",
          "image.generate",
          "image.edit",
          "video.generate",
          "fake.echo"
        ]
      },
      "ModelEntry": {
        "type": "object",
        "required": [
          "kind",
          "model",
          "provider",
          "unit_vnd",
          "enabled"
        ],
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/JobKind"
          },
          "model": {
            "type": "string",
            "description": "Tên model trong bảng giá; `*` là giá cho `auto`."
          },
          "provider": {
            "type": "string",
            "description": "`suno`, `chatgpt`, `flow`, `grok`, `fake`, hoặc `auto` khi nhiều provider cùng phục vụ dòng `*`."
          },
          "unit_vnd": {
            "type": "integer",
            "description": "Giá một đơn vị (VND)."
          },
          "enabled": {
            "type": "boolean",
            "description": "Hiện có tài khoản phục vụ được."
          }
        }
      },
      "JobList": {
        "type": "object",
        "required": [
          "jobs"
        ],
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Job"
            }
          },
          "cursor": {
            "type": "string",
            "description": "Có khi còn trang sau."
          }
        }
      },
      "Job": {
        "type": "object",
        "description": "Trạng thái job; cùng shape trong response và trong callback.",
        "required": [
          "id",
          "kind",
          "medium",
          "status",
          "model",
          "requested_model",
          "quantity",
          "delivered",
          "items",
          "request_id",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/JobKind"
          },
          "medium": {
            "type": "string",
            "enum": [
              "music",
              "image",
              "video"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "succeeded",
              "failed"
            ]
          },
          "progress": {
            "type": "string",
            "description": "Chỉ có khi job đang chạy và có tiến độ: `text` (đã có lời/tiêu đề), `first` (item đầu nghe được qua `stream_url`).",
            "enum": [
              "text",
              "first"
            ]
          },
          "model": {
            "type": "string",
            "description": "Model thực tế; rỗng khi là `auto` và job chưa kết thúc."
          },
          "requested_model": {
            "type": "string"
          },
          "quantity": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobItem"
            }
          },
          "error": {
            "$ref": "#/components/schemas/JobError"
          },
          "request_id": {
            "type": "string"
          },
          "callback_url": {
            "type": "string",
            "format": "uri"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "JobItem": {
        "type": "object",
        "description": "Kết quả đã lưu (`url`, `bytes`, `expires_at`) hoặc, khi đang tạo, preview (`stream_url`).",
        "required": [
          "index",
          "content_type"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL tải kết quả (`/files/{id}`)."
          },
          "stream_url": {
            "type": "string",
            "format": "uri",
            "description": "URL phát khi đang tạo (`/api/v1/media/{id}`)."
          },
          "content_type": {
            "type": "string"
          },
          "bytes": {
            "type": "integer"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "meta": {
            "description": "Metadata do provider trả (JSON tùy ý)."
          }
        }
      },
      "JobError": {
        "type": "object",
        "required": [
          "code",
          "message",
          "retryable"
        ],
        "properties": {
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string"
          },
          "retryable": {
            "type": "boolean"
          }
        }
      },
      "AlignedLyrics": {
        "type": "object",
        "required": [
          "words"
        ],
        "properties": {
          "words": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "word",
                "start_s",
                "end_s",
                "success",
                "p_align"
              ],
              "properties": {
                "word": {
                  "type": "string"
                },
                "start_s": {
                  "type": "number"
                },
                "end_s": {
                  "type": "number"
                },
                "success": {
                  "type": "boolean"
                },
                "p_align": {
                  "type": "number"
                }
              }
            }
          },
          "waveform": {
            "type": "array",
            "items": {
              "type": "number"
            }
          },
          "cer": {
            "type": "number",
            "description": "Tỉ lệ lỗi ký tự của việc căn lời."
          },
          "streamed": {
            "type": "boolean"
          }
        }
      },
      "CallbackURL": {
        "type": "string",
        "format": "uri",
        "description": "URL nhận callback khi job đổi giai đoạn; body là `Job`. Phải là http(s) công khai."
      },
      "MusicModel": {
        "type": "string",
        "description": "Bỏ trống/`auto` để Wave chọn. Bí danh (không phân biệt hoa thường): v2, v3, v35, v4, v45, v45plus (v4.5+), v45all, v5, v55, v6, v6wild, v6mini; hoặc khóa wire `chirp-*`. Tên khác → 400 `MODEL_UNAVAILABLE`.",
        "default": "auto"
      },
      "MusicGenerateFields": {
        "type": "object",
        "description": "Các trường chung của generate/extend/cover/infill/add_vocals/add_instrumental/upload_cover/upload_extend/mashup (Suno). Chế độ custom bật khi có `style` hoặc `title` (cần cả hai; `prompt` là lời, bắt buộc trừ khi `instrumental`). Chế độ mô tả khi không có: `prompt` bắt buộc (riêng music.generate instrumental có thể bỏ trống).",
        "properties": {
          "model": {
            "$ref": "#/components/schemas/MusicModel"
          },
          "prompt": {
            "type": "string",
            "maxLength": 5000,
            "description": "Mô tả bài hát, hoặc lời bài hát khi ở chế độ custom."
          },
          "instrumental": {
            "type": "boolean",
            "default": false
          },
          "style": {
            "type": "string",
            "maxLength": 1000
          },
          "title": {
            "type": "string",
            "maxLength": 100
          },
          "negative_style": {
            "type": "string",
            "maxLength": 1000
          },
          "duration_s": {
            "type": "integer",
            "minimum": 10,
            "maximum": 360,
            "description": "Cần chế độ custom và model v5.5/v6 (hoặc auto)."
          },
          "persona_id": {
            "type": "string"
          },
          "vocal_gender": {
            "type": "string",
            "enum": [
              "m",
              "f"
            ]
          },
          "style_weight": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "weirdness": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "audio_weight": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "MusicGenerateRequest": {
        "description": "Tạo bài hát mới (kind mặc định của /api/v1/music/jobs). Không nhận clip_id, continue_at, start_s, end_s, full_lyrics, audio_url hay part audio.",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.generate",
                "default": "music.generate"
              }
            }
          }
        ]
      },
      "MusicExtendRequest": {
        "description": "Kéo dài một clip do Wave tạo.",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind",
              "clip_id"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.extend"
              },
              "clip_id": {
                "type": "string",
                "description": "Clip nguồn; phải do Wave tạo (400 `UNKNOWN_SOURCE_CLIP` nếu không)."
              },
              "continue_at": {
                "type": "number",
                "exclusiveMinimum": 0,
                "description": "Giây bắt đầu phần nối tiếp."
              }
            }
          }
        ]
      },
      "MusicCoverRequest": {
        "description": "Cover một clip do Wave tạo.",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind",
              "clip_id"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.cover"
              },
              "clip_id": {
                "type": "string"
              },
              "start_s": {
                "type": "number",
                "minimum": 0,
                "description": "Đi cùng end_s; cửa sổ cover."
              },
              "end_s": {
                "type": "number",
                "description": "Phải lớn hơn start_s."
              }
            }
          }
        ]
      },
      "MusicInfillRequest": {
        "description": "Thay một đoạn (tối thiểu 10 giây) của clip hoặc của audio tải lên. Luôn ở chế độ custom (cần style, title, prompt). Nguồn: `clip_id` hoặc đúng một audio (`audio_url` / part `audio`).",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind",
              "start_s",
              "end_s",
              "full_lyrics"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.infill"
              },
              "clip_id": {
                "type": "string"
              },
              "audio_url": {
                "type": "string",
                "format": "uri"
              },
              "start_s": {
                "type": "number",
                "minimum": 0
              },
              "end_s": {
                "type": "number",
                "description": "end_s - start_s ≥ 10."
              },
              "full_lyrics": {
                "type": "string",
                "description": "Toàn bộ lời sau khi sửa."
              }
            }
          }
        ]
      },
      "MusicAddVocalsRequest": {
        "description": "Thêm giọng hát lên audio tải lên (đúng một: `audio_url` hoặc part `audio`). Luôn custom: cần `style`, `title`, `prompt` (lời); `instrumental` bị từ chối.",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind",
              "style",
              "title",
              "prompt"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.add_vocals"
              },
              "audio_url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        ]
      },
      "MusicAddInstrumentalRequest": {
        "description": "Tạo nhạc nền cho giọng hát tải lên (đúng một audio). Luôn custom và instrumental: cần `style`, `title`; không nhận `prompt`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind",
              "style",
              "title"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.add_instrumental"
              },
              "audio_url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        ]
      },
      "MusicUploadCoverRequest": {
        "description": "Cover audio tải lên (đúng một: `audio_url` hoặc part `audio`).",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.upload_cover"
              },
              "audio_url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        ]
      },
      "MusicUploadExtendRequest": {
        "description": "Kéo dài audio tải lên (đúng một audio). Không nhận clip_id.",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.upload_extend"
              },
              "audio_url": {
                "type": "string",
                "format": "uri"
              },
              "continue_at": {
                "type": "number",
                "exclusiveMinimum": 0
              }
            }
          }
        ]
      },
      "MusicMashupRequest": {
        "description": "Trộn đúng hai audio: `audio_urls` (2 phần tử) hoặc hai part `audio` (có thể kết hợp `audio_url` + `audio_urls`).",
        "allOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateFields"
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "music.mashup"
              },
              "audio_url": {
                "type": "string",
                "format": "uri"
              },
              "audio_urls": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uri"
                },
                "maxItems": 2
              }
            }
          }
        ]
      },
      "MusicSoundsRequest": {
        "type": "object",
        "description": "Tạo hiệu ứng âm thanh (model v5 trở lên hoặc auto).",
        "required": [
          "kind",
          "prompt"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "music.sounds"
          },
          "model": {
            "$ref": "#/components/schemas/MusicModel"
          },
          "prompt": {
            "type": "string",
            "maxLength": 500
          },
          "loop": {
            "type": "boolean",
            "default": false
          },
          "tempo": {
            "type": "integer",
            "minimum": 1,
            "maximum": 300
          },
          "key": {
            "type": "string",
            "default": "Any",
            "enum": [
              "Any",
              "C",
              "C#",
              "D",
              "D#",
              "E",
              "F",
              "F#",
              "G",
              "G#",
              "A",
              "A#",
              "B",
              "Cm",
              "C#m",
              "Dm",
              "D#m",
              "Em",
              "Fm",
              "F#m",
              "Gm",
              "G#m",
              "Am",
              "A#m",
              "Bm"
            ]
          },
          "grab_lyrics": {
            "type": "boolean",
            "default": false
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "MusicLyricsRequest": {
        "type": "object",
        "description": "Viết lời từ mô tả ngắn.",
        "required": [
          "kind",
          "prompt"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "music.lyrics"
          },
          "prompt": {
            "type": "string",
            "maxLength": 200
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "MusicClipRequest": {
        "type": "object",
        "description": "Thao tác trên một clip do Wave tạo: `music.wav` (WAV), `music.mp4` (video), `music.midi` (MIDI), `music.cover_art` (ảnh bìa).",
        "required": [
          "kind",
          "clip_id"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "music.wav",
              "music.mp4",
              "music.midi",
              "music.cover_art"
            ]
          },
          "clip_id": {
            "type": "string"
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "MusicVocalRemovalRequest": {
        "type": "object",
        "description": "Tách giọng/stem. Nguồn: đúng một trong `clip_id` hoặc audio (`audio_url` / part `audio`).",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "music.vocal_removal"
          },
          "clip_id": {
            "type": "string"
          },
          "audio_url": {
            "type": "string",
            "format": "uri"
          },
          "type": {
            "type": "string",
            "default": "separate_vocal",
            "enum": [
              "separate_vocal",
              "split_stem",
              "split_stem_advanced"
            ]
          },
          "stem_name": {
            "type": "string",
            "description": "Bắt buộc với `split_stem_advanced`, không dùng với type khác.",
            "enum": [
              "Vocals",
              "Backing Vocals",
              "Drums",
              "Bass",
              "Guitar",
              "Keyboard",
              "Percussion",
              "Strings",
              "Synth",
              "FX",
              "Brass",
              "Woodwinds"
            ]
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "MusicPersonaRequest": {
        "type": "object",
        "description": "Tạo persona giọng hát từ một clip.",
        "required": [
          "kind",
          "clip_id",
          "name",
          "description"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "music.persona"
          },
          "clip_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "vocal_start_s": {
            "type": "number",
            "minimum": 0,
            "default": 0
          },
          "vocal_end_s": {
            "type": "number",
            "default": 30,
            "description": "Phải lớn hơn vocal_start_s."
          },
          "style": {
            "type": "string"
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "FakeEchoRequest": {
        "type": "object",
        "description": "Provider giả để smoke test; chỉ có khi `providers.fake` được bật. Thuộc medium music.",
        "required": [
          "kind",
          "prompt"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "fake.echo"
          },
          "prompt": {
            "type": "string"
          },
          "model": {
            "type": "string",
            "enum": [
              "auto",
              "fake-1",
              "fake-2"
            ],
            "default": "auto"
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "default": 1
          },
          "behavior": {
            "type": "string",
            "default": "done",
            "enum": [
              "done",
              "pending",
              "fail",
              "requeue",
              "ambiguous",
              "hang",
              "stall"
            ]
          },
          "delay_ms": {
            "type": "integer"
          },
          "source_url": {
            "type": "string",
            "format": "uri"
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "MusicJobRequest": {
        "description": "Body JSON của POST /api/v1/music/jobs; `kind` chọn schema, mặc định `music.generate`.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/MusicGenerateRequest"
          },
          {
            "$ref": "#/components/schemas/MusicExtendRequest"
          },
          {
            "$ref": "#/components/schemas/MusicCoverRequest"
          },
          {
            "$ref": "#/components/schemas/MusicInfillRequest"
          },
          {
            "$ref": "#/components/schemas/MusicAddVocalsRequest"
          },
          {
            "$ref": "#/components/schemas/MusicAddInstrumentalRequest"
          },
          {
            "$ref": "#/components/schemas/MusicUploadCoverRequest"
          },
          {
            "$ref": "#/components/schemas/MusicUploadExtendRequest"
          },
          {
            "$ref": "#/components/schemas/MusicMashupRequest"
          },
          {
            "$ref": "#/components/schemas/MusicSoundsRequest"
          },
          {
            "$ref": "#/components/schemas/MusicLyricsRequest"
          },
          {
            "$ref": "#/components/schemas/MusicClipRequest"
          },
          {
            "$ref": "#/components/schemas/MusicVocalRemovalRequest"
          },
          {
            "$ref": "#/components/schemas/MusicPersonaRequest"
          },
          {
            "$ref": "#/components/schemas/FakeEchoRequest"
          }
        ],
        "discriminator": {
          "propertyName": "kind",
          "mapping": {
            "music.generate": "#/components/schemas/MusicGenerateRequest",
            "music.extend": "#/components/schemas/MusicExtendRequest",
            "music.cover": "#/components/schemas/MusicCoverRequest",
            "music.infill": "#/components/schemas/MusicInfillRequest",
            "music.add_vocals": "#/components/schemas/MusicAddVocalsRequest",
            "music.add_instrumental": "#/components/schemas/MusicAddInstrumentalRequest",
            "music.upload_cover": "#/components/schemas/MusicUploadCoverRequest",
            "music.upload_extend": "#/components/schemas/MusicUploadExtendRequest",
            "music.mashup": "#/components/schemas/MusicMashupRequest",
            "music.sounds": "#/components/schemas/MusicSoundsRequest",
            "music.lyrics": "#/components/schemas/MusicLyricsRequest",
            "music.wav": "#/components/schemas/MusicClipRequest",
            "music.mp4": "#/components/schemas/MusicClipRequest",
            "music.midi": "#/components/schemas/MusicClipRequest",
            "music.cover_art": "#/components/schemas/MusicClipRequest",
            "music.vocal_removal": "#/components/schemas/MusicVocalRemovalRequest",
            "music.persona": "#/components/schemas/MusicPersonaRequest",
            "fake.echo": "#/components/schemas/FakeEchoRequest"
          }
        }
      },
      "MusicMultipartRequest": {
        "type": "object",
        "description": "Dạng multipart của MusicJobRequest: mỗi part text là một field JSON cùng tên (hoặc part `request` chứa cả object JSON); part file `audio` là nguồn âm thanh (tối đa 20 MiB mỗi file). Tổng body ≤ 41 MiB.",
        "properties": {
          "request": {
            "type": "string",
            "description": "Cả body JSON (MusicJobRequest) trong một part."
          },
          "kind": {
            "$ref": "#/components/schemas/JobKind"
          },
          "audio": {
            "type": "array",
            "description": "1 file cho add_vocals/add_instrumental/upload_cover/upload_extend/vocal_removal/infill (không clip_id); 2 file cho mashup.",
            "items": {
              "type": "string",
              "format": "binary"
            },
            "maxItems": 2
          }
        },
        "additionalProperties": true
      },
      "MediaRef": {
        "description": "Ảnh nguồn: chuỗi URL http(s), hoặc object `{url}` / `{b64}` (chỉ một) kèm `crop` tùy chọn (flow). Grok không nhận `crop`.",
        "oneOf": [
          {
            "type": "string",
            "format": "uri"
          },
          {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "b64": {
                "type": "string",
                "description": "Ảnh mã hoá base64."
              },
              "crop": {
                "type": "object",
                "description": "Khung cắt chuẩn hoá 0..1 mỗi cạnh (flow).",
                "properties": {
                  "top": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "left": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "bottom": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1
                  },
                  "right": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              }
            }
          }
        ]
      },
      "ImageFields": {
        "type": "object",
        "description": "Body ảnh thống nhất. Provider do `model` quyết định; field provider không hỗ trợ bị từ chối (400) trừ khi `drop_unsupported: true` (khi đó ghi vào `dropped`). chatgpt (`gpt-image-1`): aspect_ratio 1:1|16:9|9:16, không nhận seed/references/negative_prompt/provider_options/resolution/start_image/end_image/duration_s. flow (`nano-banana-*`): aspect_ratio 1:1|16:9|9:16|3:4|4:3 (hoặc landscape|portrait|square), seed ≥ 0, references tối đa 10 (tính cả ảnh nguồn). grok (`grok-imagine`, `grok-imagine-pro`): chỉ image.generate, aspect_ratio 1:1|2:3|3:2|4:3|3:4|16:9|9:16, provider_options.grok.",
        "required": [
          "prompt"
        ],
        "properties": {
          "model": {
            "type": "string",
            "default": "auto",
            "description": "`auto`, `gpt-image-1`, `nano-banana-2-lite`, `nano-banana-2`, `nano-banana-pro`, `grok-imagine`, `grok-imagine-pro` (tiền tố `chatgpt/`, `flow/`, `grok/` được chấp nhận)."
          },
          "prompt": {
            "type": "string",
            "maxLength": 32000
          },
          "aspect_ratio": {
            "type": "string",
            "description": "Tỉ lệ khung; tập giá trị theo provider (xem mô tả)."
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "default": 1
          },
          "drop_unsupported": {
            "type": "boolean",
            "default": false
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "description": "flow."
          },
          "references": {
            "type": "array",
            "description": "Ảnh tham chiếu (flow, tối đa 10 kể cả nguồn edit).",
            "items": {
              "$ref": "#/components/schemas/MediaRef"
            },
            "maxItems": 10
          },
          "negative_prompt": {
            "type": "string",
            "description": "Không provider ảnh nào hỗ trợ; cần drop_unsupported."
          },
          "resolution": {
            "type": "string",
            "description": "Chỉ cho video; bị từ chối trên ảnh trừ khi drop_unsupported."
          },
          "duration_s": {
            "type": "integer",
            "description": "Chỉ cho video."
          },
          "start_image": {
            "$ref": "#/components/schemas/MediaRef"
          },
          "end_image": {
            "$ref": "#/components/schemas/MediaRef"
          },
          "provider_options": {
            "type": "object",
            "description": "Theo provider; chỉ grok dùng: `{\"grok\":{\"nsfw\",\"watermark\",\"skip_upsampler\"}}`.",
            "properties": {
              "grok": {
                "$ref": "#/components/schemas/GrokOptions"
              }
            }
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "GrokOptions": {
        "type": "object",
        "description": "Bật/tắt của Grok; mặc định đều false.",
        "properties": {
          "nsfw": {
            "type": "boolean",
            "default": false
          },
          "watermark": {
            "type": "boolean",
            "default": false
          },
          "skip_upsampler": {
            "type": "boolean",
            "default": false
          }
        },
        "additionalProperties": false
      },
      "ImageGenerateRequest": {
        "description": "Tạo ảnh (kind mặc định của /api/v1/images/jobs). Không nhận `image_url` hay part `image`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ImageFields"
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "const": "image.generate",
                "default": "image.generate"
              }
            }
          }
        ]
      },
      "ImageEditRequest": {
        "description": "Sửa ảnh (chatgpt hoặc flow; grok không có). Đúng một nguồn: `image_url` hoặc part `image`. Với chatgpt, `aspect_ratio` bị bỏ (ghi vào dropped) vì upstream lấy theo ảnh nguồn.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ImageFields"
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "const": "image.edit"
              },
              "image_url": {
                "type": "string",
                "format": "uri",
                "description": "URL http(s) của ảnh nguồn; Wave tải về (tối đa 20 MiB)."
              }
            }
          }
        ]
      },
      "ImageJobRequest": {
        "description": "Body JSON của POST /api/v1/images/jobs; `kind` mặc định `image.generate`.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/ImageGenerateRequest"
          },
          {
            "$ref": "#/components/schemas/ImageEditRequest"
          }
        ],
        "discriminator": {
          "propertyName": "kind",
          "mapping": {
            "image.generate": "#/components/schemas/ImageGenerateRequest",
            "image.edit": "#/components/schemas/ImageEditRequest"
          }
        }
      },
      "ImageMultipartRequest": {
        "type": "object",
        "description": "Dạng multipart của ImageJobRequest: part text = field JSON (hoặc part `request`); part file `image` là nguồn của image.edit (đúng một), part file `reference` là ảnh tham chiếu (flow).",
        "properties": {
          "request": {
            "type": "string",
            "description": "Cả body JSON (ImageJobRequest) trong một part."
          },
          "kind": {
            "type": "string",
            "enum": [
              "image.generate",
              "image.edit"
            ]
          },
          "image": {
            "type": "string",
            "format": "binary"
          },
          "reference": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "binary"
            }
          }
        },
        "additionalProperties": true
      },
      "VideoGenerateRequest": {
        "type": "object",
        "description": "Body video thống nhất (kind `video.generate`). flow (`veo-3.1-lite`, `veo-3.1-fast` (mặc định auto), `veo-3.1-quality`, `omni-flash`): aspect_ratio 16:9|9:16 (landscape|portrait), duration_s 4|6|8 (lite/fast), 8 (quality), 4|6|8|10 (omni-flash), mặc định 8; resolution 360p|720p chỉ omni-flash; ảnh vào tối đa 3 (lite/fast), 0 (quality), 7 (omni-flash); references tối đa 7; seed/negative_prompt/provider_options không hỗ trợ. grok (`grok-imagine-video`): count phải 1, aspect_ratio 1:1|2:3|3:2|16:9|9:16, duration_s 6|10|15 (mặc định 6), resolution 480p|720p (mặc định 480p), chỉ start_image (không crop), không references/end_image/seed/negative_prompt. `prompt` bắt buộc khi không có ảnh nào (text-to-video).",
        "properties": {
          "kind": {
            "type": "string",
            "const": "video.generate",
            "default": "video.generate"
          },
          "model": {
            "type": "string",
            "default": "auto"
          },
          "prompt": {
            "type": "string",
            "maxLength": 32000
          },
          "aspect_ratio": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "default": 1
          },
          "drop_unsupported": {
            "type": "boolean",
            "default": false
          },
          "duration_s": {
            "type": "integer",
            "description": "Giây; tập giá trị theo model."
          },
          "resolution": {
            "type": "string",
            "description": "`360p`|`720p` (flow omni-flash), `480p`|`720p` (grok)."
          },
          "start_image": {
            "$ref": "#/components/schemas/MediaRef"
          },
          "end_image": {
            "$ref": "#/components/schemas/MediaRef"
          },
          "image_url": {
            "type": "string",
            "format": "uri",
            "description": "Bí danh cũ của `start_image` (URL); không gửi cùng start_image."
          },
          "references": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MediaRef"
            },
            "maxItems": 7,
            "description": "Ingredients (flow); không dùng cùng start_image/end_image."
          },
          "seed": {
            "type": "integer"
          },
          "negative_prompt": {
            "type": "string"
          },
          "provider_options": {
            "type": "object",
            "properties": {
              "grok": {
                "$ref": "#/components/schemas/GrokOptions"
              }
            }
          },
          "callback_url": {
            "$ref": "#/components/schemas/CallbackURL"
          }
        }
      },
      "VideoMultipartRequest": {
        "type": "object",
        "description": "Dạng multipart của VideoGenerateRequest: part text = field JSON (hoặc part `request`); part file `start_image`, `end_image` (tối đa một mỗi loại, không gửi trùng với body) và `reference` (nhiều).",
        "properties": {
          "request": {
            "type": "string"
          },
          "start_image": {
            "type": "string",
            "format": "binary"
          },
          "end_image": {
            "type": "string",
            "format": "binary"
          },
          "reference": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "binary"
            }
          }
        },
        "additionalProperties": true
      },
      "OpenAIImageGenerationRequest": {
        "type": "object",
        "description": "Body theo OpenAI Images. Field lạ → 400. `size`, `quality`, `background`, `output_format`, `output_compression`, `moderation`, `partial_images`, `style`, `user` được nhận và bỏ qua.",
        "required": [
          "prompt"
        ],
        "additionalProperties": false,
        "properties": {
          "prompt": {
            "type": "string",
            "maxLength": 32000
          },
          "model": {
            "type": "string",
            "description": "Chỉ truyền tiếp khi là model Wave phục vụ cho image.generate; tên OpenAI (dall-e-3...) rơi về auto."
          },
          "n": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "default": 1
          },
          "response_format": {
            "type": "string",
            "enum": [
              "b64_json",
              "url"
            ],
            "default": "b64_json"
          },
          "stream": {
            "type": "boolean",
            "description": "Phải là false."
          },
          "ratio": {
            "type": "string",
            "description": "Mở rộng của gateway cũ, thành `aspect_ratio` (vd 1:1, 16:9, 9:16)."
          },
          "size": {
            "type": "string"
          },
          "quality": {
            "type": "string"
          },
          "background": {
            "type": "string"
          },
          "output_format": {
            "type": "string"
          },
          "output_compression": {
            "type": "integer"
          },
          "moderation": {
            "type": "string"
          },
          "partial_images": {
            "type": "integer"
          },
          "style": {
            "type": "string"
          },
          "user": {
            "type": "string"
          }
        }
      },
      "OpenAIImageEditRequest": {
        "type": "object",
        "required": [
          "image",
          "prompt"
        ],
        "properties": {
          "image": {
            "type": "string",
            "format": "binary",
            "description": "Đúng một file ảnh nguồn (tối đa 20 MiB)."
          },
          "prompt": {
            "type": "string",
            "maxLength": 32000
          },
          "model": {
            "type": "string",
            "description": "Chỉ truyền tiếp khi là model Wave phục vụ cho image.edit."
          },
          "n": {
            "type": "integer",
            "const": 1,
            "default": 1
          },
          "response_format": {
            "type": "string",
            "enum": [
              "b64_json",
              "url"
            ],
            "default": "b64_json"
          },
          "stream": {
            "type": "boolean",
            "description": "Phải là false."
          }
        },
        "additionalProperties": true
      },
      "OpenAIImagesResponse": {
        "type": "object",
        "required": [
          "created",
          "data",
          "output_format"
        ],
        "properties": {
          "created": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "b64_json": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "output_format": {
            "type": "string",
            "enum": [
              "png",
              "jpeg",
              "webp"
            ]
          }
        }
      }
    }
  }
}
