openapi: 3.0.3 # ============================================================================= # Kneron Converter — Phase 1 對外 API # # 此 spec 對齊「實作端」(apps/task-scheduler/src/routes/v1/`), # 與 TDD.md §1 在以下幾處刻意不一致(皆為實作端進化、TDD 尚未同步): # # 1. status 映射對外只用 `created` / `running` / `completed` / `failed` # (TDD §2.7.1 仍提到內部大寫 ONNX/BIE/NEF;對外 spec 不暴露內部值) # 2. GET /jobs 採 cursor-based 分頁(base64-url-encoded opaque string), # 非 TDD §1.4.4 的 offset 欄位 # 3. GET /jobs 列表回應使用 `{jobs, total, next_cursor}`,非 TDD 的 # `{items, limit, offset, total}` # 4. GET /jobs/:id 回應實作端會 strip 內部欄位 `created_by_client_id`, # 不對外曝露 # 5. model 副檔名只接受 `.onnx` / `.tflite`(PRD §4.1 F-01;TDD §1.4.2 # 列了 6 種但 PRD 才是對 user-facing 的合約 — doc-review m6 已記錄) # 6. ref_image per-file 上限 10MB(Sec C2)— TDD §1.4.2 沒明確此限制 # 7. promote response 不含 part-failure 結構;採「全成功 200 / 任一失敗 502」 # 模型(Phase 1 簡化決策,TDD §1.4.5 範例描述部分失敗為 502 + details, # 實作端因 stream 模型難以原子化已改採全失敗 502 + classifyFaaError) # # 兼容說明:spec 為對外契約來源,當 TDD 與實作不一致時,以實作 + 此 spec 為準。 # 預期下輪 TDD 維護會同步上述差異。 # ============================================================================= info: title: Kneron Converter API description: | Kneron Model Converter 對外 API(Phase 1)。 本服務提供深度學習模型的轉檔服務(ONNX → BIE → NEF),目標是讓上游 應用(如 visionA-backend)能: 1. 上傳原始模型 + 參考圖片 → 建立轉檔 job 2. Polling job 狀態,直到 `completed` 或 `failed` 3. 把成功的結果檔 promote(推送)到 File Access Agent / NAS 模型庫 ## 認證(Phase 0.8b 起) 所有 `/api/v1/*` 端點都需要 `Authorization: Bearer `。 Converter 採 **pre-shared API key**(1:1 internal trust,取代 OAuth resource-server 模式)。 API key 即「caller 是 visionA」的完整證明,不分 read/write scope,不檢查 audience / tenant。 - 產生:`openssl rand -hex 32`(64 hex chars / 128 bits 熵) - visionA 與 converter 兩端使用**完全相同字串** - 詳見 `docs/autoflow/04-architecture/auth.md` ### Audit log(Phase 0.8b A7) 每個 `/api/v1/*` request(成功 / 失敗皆然)都會寫一筆 audit log,含 `source_ip`、 `token_fingerprint`(sha256 前 12 hex chars、不可逆推 token)、`request_id`、 `http_method`、`http_path`。詳見 README §7.1.5。對 visionA 端 awareness:請避免 在不同 caller instance 間混用同一個 IP 來源(否則 forensic 區分能力下降)。 > **歷史**:Phase 0.8b 之前曾規劃 OAuth `client_credentials` + JWT 驗證 > (`aud=kneron_converter_api`、`scope=converter:job.{read,write}`), > 但因 1:1 trust 場景下 OAuth 過度設計、且 stage 撞 4 個 blocker(見 visionA repo > `ADR-014` / `ADR-015` v2.1),改採 API key。converter → FAA 的 OAuth > `client_credentials` 鏈條(promote 階段內部用)**保留不動**。 ## user_id 與 trust boundary `user_id` 不是來自 auth credential(API key 也無 user 概念),而是 multipart form field(POST)或 query string(GET)。Converter **完全信任**呼叫端帶來的 user_id 是 對的,不做 user 層級 ACL。 這是 Phase 1 刻意接受的設計風險,詳見: `.autoflow/04-architecture/security.md` § Trust Boundary。 ## 錯誤格式 所有 4xx / 5xx 一律使用: ```json { "error": { "code": "snake_case_code", "message": "human readable zh-TW", "details": { "..." }, "request_id": "uuid-v4" } } ``` `details` 欄位視 code 而定(schema 中各 example 已展示)。 ## Phase 1 已知限制 / 接受風險 - `user_id` 信任邊界:見 security.md - 大檔上傳(500MB)依賴 Nginx `client_max_body_size`,後端 multer 在 OAuth 驗證前已開始 buffer;建議 Nginx vhost 設 `client_max_body_size 600M` - 單 Scheduler instance(per-process rate limiter / upload concurrency) - Crash 即 Reset:未完成的 job 在 Scheduler / Worker crash 後不保證恢復 version: 1.0.0 contact: name: Kneron Converter Team license: name: MIT servers: - url: https://your-converter.example.com description: 部署範例 — 替換為實際 Converter public host - url: http://localhost:4000 description: 本地開發(無 Nginx 反代理) # ============================================================================= # Tags # ============================================================================= tags: - name: Health description: 服務健康檢查(公開、無需認證) - name: Jobs description: 轉檔 job 生命週期管理 - name: Promote description: 結果檔搬移到 File Access Agent - name: Phase 2 (Reserved) description: 預留路由,Phase 1 一律回 501 not_implemented # ============================================================================= # 全域 security default:除標明 security: [] 外,所有 path 都需 ApiKeyAuth(Bearer scheme) # ============================================================================= security: - ApiKeyAuth: [] paths: # =========================================================================== # Health # =========================================================================== /health: get: tags: [Health] summary: 健康檢查 operationId: getHealth description: | 回傳服務與依賴(Redis / Member Center / File Access Agent)健康狀態。 - **公開**:不需要 Authorization - **三層 status**: - `healthy`:所有依賴連通 → HTTP 200 - `degraded`:Redis 連通但 MC / FAA 任一不可達 → HTTP 200 - `unhealthy`:Redis 不連通 → HTTP 503 MC / FAA 使用背景 polling(每 30 秒)+ cache,呼叫此端點本身永遠 < 5ms。 部署初期(first poll 完成前)狀態為 `pending`。 security: [] responses: '200': description: 服務健康(healthy 或 degraded) content: application/json: schema: $ref: '#/components/schemas/HealthSnapshot' examples: healthy: summary: 所有依賴正常 value: service: task-scheduler status: healthy timestamp: '2026-04-25T12:00:00Z' redis: connected version: '1.0.0' dependencies: redis: connected member_center: reachable file_access_agent: reachable degraded: summary: Redis OK 但 FAA 不可達 value: service: task-scheduler status: degraded timestamp: '2026-04-25T12:00:00Z' redis: connected version: '1.0.0' dependencies: redis: connected member_center: reachable file_access_agent: unreachable '503': description: 服務不健康(Redis 斷線) content: application/json: schema: $ref: '#/components/schemas/HealthSnapshot' examples: unhealthy: summary: Redis 斷線 value: service: task-scheduler status: unhealthy timestamp: '2026-04-25T12:00:00Z' redis: disconnected version: '1.0.0' dependencies: redis: disconnected member_center: reachable file_access_agent: reachable # =========================================================================== # POST /api/v1/jobs — 建立轉檔 job # =========================================================================== /api/v1/jobs: post: tags: [Jobs] summary: 建立轉檔 job operationId: createJob description: | 以 multipart/form-data 上傳原始模型(+ 可選的參考圖片)+ 參數, 建立一個轉檔 job,後續會自動跑 ONNX → BIE → NEF pipeline。 ## 同 user 同時只能有一個進行中 job 若 `user_id` 已有未完成 job(`created` 或 `running` 狀態),會回 409 `user_has_active_job` 並附上既有 job 詳情。 ## 大檔處理 - `model` 單檔 ≤ 500 MB(multer `LIMIT_FILE_SIZE`) - `ref_images[]` 每張 ≤ 10 MB、共 ≤ 100 張 - 部署層 Nginx vhost 應設 `client_max_body_size 600M` - 建議 client 使用 chunked transfer + 連線重試 ## 寫入順序(M5 方案 A) 1. validate auth + multipart fields 2. 先寫 MinIO(input + ref_images)— 失敗 → 502 storage_unavailable, Redis 完全乾淨 3. Lua script 原子寫入 active_job lock + job record + user index 4. enqueue 第一階段(onnx) 5. 回 201 `created` security: - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/XRequestId' requestBody: required: true content: multipart/form-data: schema: $ref: '#/components/schemas/CreateJobRequest' encoding: model: contentType: application/octet-stream 'ref_images[]': contentType: image/jpeg, image/png, application/octet-stream examples: minimal: summary: 最小必填欄位 description: | curl 範例: ``` curl -X POST https://your-converter.example.com/api/v1/jobs \ -H "Authorization: Bearer " \ -F "model=@./model.onnx" \ -F "user_id=alice" \ -F "model_id=1001" \ -F "version=v1.0.0" \ -F "platform=520" ``` with_ref_images: summary: 含 ref_images 與所有 enable flag description: | ``` curl -X POST https://your-converter.example.com/api/v1/jobs \ -H "Authorization: Bearer " \ -F "model=@./model.onnx" \ -F "ref_images[]=@./img_0.jpg" \ -F "ref_images[]=@./img_1.jpg" \ -F "user_id=alice" \ -F "model_id=1001" \ -F "version=v1.0.0" \ -F "platform=520" \ -F "enable_evaluate=true" \ -F "enable_sim_fp=false" ``` responses: '201': description: Job 建立成功 headers: X-Request-Id: $ref: '#/components/headers/XRequestId' X-RateLimit-Limit: $ref: '#/components/headers/XRateLimitLimit' X-RateLimit-Remaining: $ref: '#/components/headers/XRateLimitRemaining' content: application/json: schema: $ref: '#/components/schemas/CreateJobResponse' examples: created: value: job_id: '550e8400-e29b-41d4-a716-446655440000' status: created stage: onnx progress: 0 created_at: '2026-04-25T12:00:00Z' expires_at: '2026-05-02T12:00:00Z' user_id: alice '400': description: 欄位驗證失敗 / multipart 解析失敗 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: validation_error: summary: model_id 不在範圍 value: error: code: validation_error message: 欄位驗證失敗 details: fields: - field: model_id message: model_id 範圍必須在 1 ~ 65535 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff invalid_multipart: summary: 不是 multipart value: error: code: invalid_multipart message: multipart 解析失敗:LIMIT_UNEXPECTED_FILE details: code: LIMIT_UNEXPECTED_FILE field: model request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '401': $ref: '#/components/responses/Unauthorized' '409': description: 該 user_id 已有進行中 job content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: user_has_active_job: value: error: code: user_has_active_job message: 使用者目前已有進行中的轉檔任務 details: active_job_id: 550e8400-e29b-41d4-a716-446655440000 active_job_status: running active_job_stage: bie active_job_progress: 45 active_job_created_at: '2026-04-25T12:00:00Z' request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '413': description: 上傳檔案超過大小上限 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: model_too_large: summary: model 檔超過 500MB value: error: code: file_too_large message: 上傳檔案超過 500MB 上限 details: field: model limit_bytes: 524288000 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff ref_image_too_large: summary: 單張 ref_image 超過 10MB value: error: code: file_too_large message: ref_image 超過單張 10485760 bytes 上限 details: field: 'ref_images[3]' size_bytes: 12000000 limit_bytes: 10485760 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '429': $ref: '#/components/responses/RateLimited' '500': description: 伺服器設定錯或其他內部錯誤 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: misconfiguration: value: error: code: misconfiguration message: POST /api/v1/jobs 需 STORAGE_BACKEND=minio request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '502': description: 物件儲存(MinIO)短暫無法寫入 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: storage_unavailable: value: error: code: storage_unavailable message: 檔案儲存服務暫時無法使用,請稍後重試 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '503': description: | 兩種情境: - `service_busy`:並發 upload 超過 process semaphore 上限 - `service_unavailable`:server 端 `CONVERTER_API_KEY` env 未設定(fail-secure) content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: service_busy: value: error: code: service_busy message: 系統繁忙中,請稍後重試 details: retry_after_seconds: 30 max_concurrent: 5 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff api_key_not_configured: value: error: code: service_unavailable message: API key not configured request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff # ========================================================================= # GET /api/v1/jobs — Recovery 列表(user_id 必填) # ========================================================================= get: tags: [Jobs] summary: 列出 job(Recovery / polling) operationId: listJobs description: | 列出指定 `user_id` 的 job 清單,支援 status 過濾與 cursor-based 分頁。 ## 為什麼 user_id 必填 Phase 1 強制 user_id 必填,避免「不帶條件的全掃」與資訊洩露。 未來若新增 admin scope 可再放寬(TDD §8.1 預留)。 ## 隔離 列出的 job 一律自動以呼叫端 `client_id` 過濾 — 即同一 user_id 但由不同 client 建立的 job 不會出現在結果中。Phase 0.8b A3 起,API key 路線下 `client_id` 寫死為 `'visionA-service'`;隔離邏輯仍保留供未來多 caller 擴展用。 ## 分頁 使用 base64-url-encoded opaque cursor。client 不該假設 cursor 內容格式(未來可能改為 keyset)。當沒有更多資料時 `next_cursor: null`。 security: - ApiKeyAuth: [] parameters: - name: user_id in: query required: true description: | 必填。1-128 字元,只允許 `^[A-Za-z0-9._-]+$`(嚴格白名單, 擋 XSS / Redis key injection / log injection) schema: type: string pattern: '^[A-Za-z0-9._-]{1,128}$' example: alice - name: status in: query required: false description: | 過濾條件: - `in_progress`:預設值,相當於 `created` ∪ `running` - `completed` / `failed` / `all` schema: type: string enum: [in_progress, completed, failed, all] default: in_progress - name: limit in: query required: false description: 一頁筆數,1-50(預設 10) schema: type: integer minimum: 1 maximum: 50 default: 10 - name: cursor in: query required: false description: | base64-url-encoded opaque cursor。從上一頁的 `next_cursor` 欄位取得。第一頁不要帶。 schema: type: string example: eyJvZmZzZXQiOjEwfQ - $ref: '#/components/parameters/XRequestId' responses: '200': description: 查詢成功 headers: X-Request-Id: $ref: '#/components/headers/XRequestId' X-RateLimit-Limit: $ref: '#/components/headers/XRateLimitLimit' X-RateLimit-Remaining: $ref: '#/components/headers/XRateLimitRemaining' content: application/json: schema: $ref: '#/components/schemas/ListJobsResponse' examples: with_results: value: jobs: - job_id: 550e8400-e29b-41d4-a716-446655440000 user_id: alice status: running stage: bie progress: 45 stage_progress: 60 created_at: '2026-04-25T12:00:00Z' updated_at: '2026-04-25T12:05:30Z' expires_at: '2026-05-02T12:00:00Z' stage_timings: onnx: started_at: '2026-04-25T12:00:00Z' completed_at: '2026-04-25T12:02:10Z' bie: started_at: '2026-04-25T12:02:15Z' completed_at: null nef: started_at: null completed_at: null input: filename: model.onnx object_key: jobs/550e8400-e29b-41d4-a716-446655440000/input/model.onnx size_bytes: 204800000 ref_images_count: 0 result_object_keys: null error: null parameters: model_id: 1001 version: v1.0.0 platform: '520' enable_evaluate: false enable_sim_fp: false enable_sim_fixed: false enable_sim_hw: false metadata: {} total: 1 next_cursor: null empty: value: jobs: [] total: 0 next_cursor: null '400': description: 查詢參數驗證失敗(user_id 缺漏、status 不在 enum 等) content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: missing_user_id: value: error: code: validation_error message: 查詢參數驗證失敗 details: fields: - field: user_id message: user_id 必填,1-128 字元,僅可包含英數字 / `.` / `_` / `-` request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/RateLimited' '503': $ref: '#/components/responses/ServiceUnavailable' # =========================================================================== # GET /api/v1/jobs/:id — 單一 job 狀態 + ETag # =========================================================================== /api/v1/jobs/{id}: parameters: - $ref: '#/components/parameters/JobIdPath' get: tags: [Jobs] summary: 取得單一 job 狀態 operationId: getJob description: | 取得單一 job 詳情。支援 ETag 304。 ## Client 隔離 即使 jobId 真實存在,若呼叫端 `client_id` 與 job 的 `created_by_client_id` 不符,**一律回 404**(不洩漏存在性)。Phase 0.8b A3 起,API key 路線下 `client_id` 寫死為 `'visionA-service'`。 ## ETag - response 帶 `ETag: W/""`(weak ETag) - client 後續 polling 帶 `If-None-Match: ` → 若 job 未變化回 304(無 body) security: - ApiKeyAuth: [] parameters: - name: If-None-Match in: header required: false description: 上次 polling 拿到的 ETag。命中則回 304。 schema: type: string example: W/"a1b2c3d4..." - $ref: '#/components/parameters/XRequestId' responses: '200': description: Job 詳情 headers: ETag: schema: type: string description: weak ETag,hash(updated_at) X-Request-Id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/Job' examples: running: value: job_id: 550e8400-e29b-41d4-a716-446655440000 user_id: alice status: running stage: bie progress: 45 stage_progress: 60 created_at: '2026-04-25T12:00:00Z' updated_at: '2026-04-25T12:05:30Z' expires_at: '2026-05-02T12:00:00Z' stage_timings: onnx: started_at: '2026-04-25T12:00:00Z' completed_at: '2026-04-25T12:02:10Z' bie: started_at: '2026-04-25T12:02:15Z' completed_at: null nef: started_at: null completed_at: null input: filename: model.onnx object_key: jobs/550e8400-e29b-41d4-a716-446655440000/input/model.onnx size_bytes: 204800000 ref_images_count: 0 result_object_keys: null error: null parameters: model_id: 1001 version: v1.0.0 platform: '520' enable_evaluate: false enable_sim_fp: false enable_sim_fixed: false enable_sim_hw: false metadata: {} completed: value: job_id: 550e8400-e29b-41d4-a716-446655440000 user_id: alice status: completed stage: null progress: 100 stage_progress: 100 created_at: '2026-04-25T12:00:00Z' updated_at: '2026-04-25T12:08:30Z' expires_at: '2026-05-02T12:00:00Z' stage_timings: onnx: started_at: '2026-04-25T12:00:00Z' completed_at: '2026-04-25T12:02:10Z' bie: started_at: '2026-04-25T12:02:15Z' completed_at: '2026-04-25T12:05:00Z' nef: started_at: '2026-04-25T12:05:05Z' completed_at: '2026-04-25T12:08:30Z' input: filename: model.onnx object_key: jobs/550e8400-e29b-41d4-a716-446655440000/input/model.onnx size_bytes: 204800000 ref_images_count: 0 result_object_keys: onnx: jobs/550e8400-e29b-41d4-a716-446655440000/output/model.onnx bie: jobs/550e8400-e29b-41d4-a716-446655440000/output/model.bie nef: jobs/550e8400-e29b-41d4-a716-446655440000/output/model.nef error: null parameters: model_id: 1001 version: v1.0.0 platform: '520' enable_evaluate: false enable_sim_fp: false enable_sim_fixed: false enable_sim_hw: false metadata: {} failed: value: job_id: 550e8400-e29b-41d4-a716-446655440000 user_id: alice status: failed stage: bie progress: 33 stage_progress: 0 created_at: '2026-04-25T12:00:00Z' updated_at: '2026-04-25T12:03:00Z' expires_at: '2026-05-02T12:00:00Z' stage_timings: onnx: started_at: '2026-04-25T12:00:00Z' completed_at: '2026-04-25T12:02:10Z' bie: started_at: '2026-04-25T12:02:15Z' completed_at: null nef: started_at: null completed_at: null input: filename: model.onnx object_key: jobs/550e8400-e29b-41d4-a716-446655440000/input/model.onnx size_bytes: 204800000 ref_images_count: 0 result_object_keys: null error: stage: bie code: quantization_failed message: 參考圖片不足或格式不符,BIE 量化階段失敗 parameters: model_id: 1001 version: v1.0.0 platform: '520' enable_evaluate: false enable_sim_fp: false enable_sim_fixed: false enable_sim_hw: false metadata: {} '304': description: ETag 命中,job 自上次取以來無變化 headers: ETag: schema: type: string '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/JobNotFound' '429': $ref: '#/components/responses/RateLimited' '503': $ref: '#/components/responses/ServiceUnavailable' delete: tags: [Phase 2 (Reserved)] summary: '[Phase 2] 取消 / 刪除 job' operationId: deleteJob description: | Phase 2 規劃的端點。Phase 1 一律回 501 `not_implemented`。 deprecated: false security: - ApiKeyAuth: [] responses: '501': $ref: '#/components/responses/NotImplemented' # =========================================================================== # POST /api/v1/jobs/:id/promote — 把結果檔搬到 FAA # =========================================================================== /api/v1/jobs/{id}/promote: parameters: - $ref: '#/components/parameters/JobIdPath' post: tags: [Promote] summary: 把成功結果檔 PUT 到 File Access Agent operationId: promoteJob description: | 把已完成(`status=completed`)的 job 指定 stage 結果檔,stream PUT 到 File Access Agent(NAS 模型庫)。 ## 冪等 - 若 job 已 promoted 過 → 直接回 200 + 既有 promoted_object_keys (不重打 FAA、不重新讀 MinIO) - 同 `target_object_key` 多次 PUT,FAA 端會覆蓋(client 安全重試) ## 序列執行 多 target 在 server 端**序列**處理(避免對 FAA 並發壓力與 OOM)。 ## 重試策略 FAA 5xx / network timeout:server 端內部已重試最多 2 次(500ms / 2s 指數退避)。client 不需要再重試 502 之外的 case。 FAA 401:server 自動 invalidate token + 重取一次。仍 401 → 503 `auth_service_unavailable`。 security: - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/XRequestId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PromoteRequest' examples: single_target: value: targets: - source: nef target_object_key: visionA/models/alice/m-1001/v1.0.0/out.nef multi_target: value: targets: - source: bie target_object_key: visionA/models/alice/m-1001/v1.0.0/out.bie - source: nef target_object_key: visionA/models/alice/m-1001/v1.0.0/out.nef responses: '200': description: Promote 成功(或冪等命中) headers: X-Request-Id: $ref: '#/components/headers/XRequestId' content: application/json: schema: $ref: '#/components/schemas/PromoteResponse' examples: success: value: job_id: 550e8400-e29b-41d4-a716-446655440000 promoted: - source: nef target_object_key: visionA/models/alice/m-1001/v1.0.0/out.nef size_bytes: 10485760 file_access_agent_etag: 'abc123' promoted_at: '2026-04-25T12:30:00Z' '400': description: targets 格式錯誤 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: empty_targets: value: error: code: validation_error message: targets 不可為空 details: fields: - field: targets message: must contain at least 1 item request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/JobNotFound' '409': description: Job 尚未完成或指定 source 沒有產出 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: not_ready: value: error: code: job_not_ready_for_promote message: Job 尚未完成,無法 promote details: current_status: ONNX request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff source_not_available: value: error: code: source_not_available message: Job 沒有 bie 階段的結果可 promote details: source: bie request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '422': description: target_object_key 格式不合法 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: invalid_object_key: value: error: code: invalid_object_key message: target_object_key 格式不合法 details: field: 'targets[0].target_object_key' reason: 不可為空、不可含 .. / 反斜線 / 控制字元 / 開頭斜線 / ? / # / %;長度 ≤ 1024 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '429': $ref: '#/components/responses/RateLimited' '502': description: File Access Agent 不可用或拒絕請求 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: gateway_unavailable: value: error: code: file_gateway_unavailable message: 檔案存取服務暫時無法使用,請稍後重試 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff gateway_rejected: summary: FAA 4xx 拒絕(如 target_object_key 命名違規) value: error: code: file_gateway_unavailable message: 檔案存取服務拒絕此請求 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '503': description: | 兩種情境: - `auth_service_unavailable`:promote 階段 converter → Member Center 取 FAA token 失敗 - `service_unavailable`:server 端 `CONVERTER_API_KEY` env 未設定(fail-secure) content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: auth_unavailable: value: error: code: auth_service_unavailable message: 認證服務目前無法簽發必要 token,請稍後重試 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff api_key_not_configured: value: error: code: service_unavailable message: API key not configured request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff # =========================================================================== # Phase 0.8b Phase B — GET /api/v1/jobs/{id}/result(streaming proxy) # =========================================================================== /api/v1/jobs/{id}/result: parameters: - $ref: '#/components/parameters/JobIdPath' get: tags: [Result] summary: 'Stream NEF result binary for visionA-backend' operationId: getJobResult description: | Phase 0.8b Phase B 新增(取代 Phase 2 `/download-tokens`)。 visionA-backend 用此 endpoint 從 Converter Bucket 直接 streaming NEF 結果檔。 取代「visionA → 拿 delegated download token → FAA」路徑(該路徑因 MC 沒實作而從未跑通)。 **安全限制**(per `token_fingerprint = sha256(api_key).slice(0,12)`): - Burst rate limit:5 req / 10s(429 + `limit_type: burst`) - Sustained rate limit:20 req / 1min(429 + `limit_type: sustained`) - Hourly bandwidth quota:1 GB / hr(429 + `limit_type: bandwidth_hourly`) - Daily bandwidth quota:6 GB / 24hr(429 + `limit_type: bandwidth_daily`) - Concurrent stream cap:10 同時 stream / instance(503 + `Retry-After: 30`) - Stream response timeout:5 分鐘(destroy connection) **Range header 處理**:silently ignored、永遠回 200 整段、設 `Accept-Ranges: none` (不回 416、不切片 MinIO request)。收到 Range header 時寫 audit log `result.range_attempted`(INFO、forensic baseline)。 詳見 `docs/autoflow/04-architecture/api/api-result.md`。 security: - ApiKeyAuth: [] responses: '200': description: NEF binary stream(完整檔、不支援 Range / partial content) headers: Content-Type: schema: type: string example: application/octet-stream Content-Length: schema: type: integer description: NEF 物件 size bytes(從 MinIO 取得) Content-Disposition: schema: type: string example: 'attachment; filename="yolov5s_720.nef"; filename*=UTF-8''''yolov5s_720.nef' description: | `attachment; filename="_.nef"; filename*=UTF-8''` - `filename`:ASCII-safe + quote-escape - `filename*`:RFC 5987 extended(為未來 unicode 預留) - 缺 source_filename 或 platform → fallback `job_.nef` Accept-Ranges: schema: type: string example: none description: '明示不支援 Range request(RFC 7233 §2.3)' content: application/octet-stream: schema: type: string format: binary '401': $ref: '#/components/responses/Unauthorized' '404': description: | - `job_not_found`:jobID 不存在 - `result_not_found`:completed 但 result_object_keys 內沒 NEF content: application/json: schema: $ref: '#/components/schemas/Error' examples: JobNotFound: value: error: code: job_not_found message: Job 550e8400-e29b-41d4-a716-446655440000 not found request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff ResultNotFound: value: error: code: result_not_found message: Job 550e8400-e29b-41d4-a716-446655440000 completed but no NEF result available request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '409': description: Job 還沒完成(status !== 'COMPLETED') content: application/json: schema: $ref: '#/components/schemas/Error' examples: NotCompleted: value: error: code: job_not_completed message: 'Job 550e8400-e29b-41d4-a716-446655440000 is ONNX; result only available after completion' details: current_status: ONNX request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '410': description: NEF 已過期(converter MinIO lifecycle 清掉 / expires_at < now) content: application/json: schema: $ref: '#/components/schemas/Error' examples: Expired: value: error: code: result_expired message: 'Job 550e8400-e29b-41d4-a716-446655440000 result expired at 2026-05-10T00:00:00Z; re-convert to get a fresh result' request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '429': description: | Rate limit 超限或 bandwidth quota 超限。 - `rate_limit_exceeded`:req-based limit 超限(`limit_type: burst | sustained`) - `bandwidth_quota_exceeded`:頻寬 quota 超限(`limit_type: bandwidth_hourly | bandwidth_daily`) headers: Retry-After: schema: type: integer description: 建議 retry 時間(秒) content: application/json: schema: $ref: '#/components/schemas/Error' examples: BurstLimitHit: value: error: code: rate_limit_exceeded message: 請求頻率過高,請稍後再試 details: limit_type: burst retry_after_seconds: 10 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff BandwidthQuotaHit: value: error: code: bandwidth_quota_exceeded message: 下載額度已用完,請稍後再試 details: limit_type: bandwidth_hourly retry_after_seconds: 2847 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '502': description: MinIO 暫時不可用(5xx / 連線錯誤) content: application/json: schema: $ref: '#/components/schemas/Error' examples: StorageUnavailable: value: error: code: storage_unavailable message: 無法讀取結果檔,請稍後重試 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff '503': description: | - `service_busy`:並發 stream 達上限(concurrent cap) - `stream_timeout`:response stream 5min timeout - `service_unavailable`:CONVERTER_API_KEY 未配置 headers: Retry-After: schema: type: integer content: application/json: schema: $ref: '#/components/schemas/Error' examples: ServiceBusy: value: error: code: service_busy message: 伺服器忙碌中,請稍後再試 details: limit_type: concurrent retry_after_seconds: 30 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff # =========================================================================== # Phase 2 預留端點 — 一律 501 not_implemented # =========================================================================== /api/v1/jobs/{id}/download-tokens: parameters: - $ref: '#/components/parameters/JobIdPath' post: tags: [Phase 2 (Reserved)] summary: '[Phase 2] 換 delegated download token' operationId: createDownloadTokens description: | Phase 2 規劃的端點,待 Member Center 完成 delegated token 流程後實作。 Phase 1 一律回 501 `not_implemented`。 deprecated: false security: - ApiKeyAuth: [] responses: '501': $ref: '#/components/responses/NotImplemented' # ============================================================================= # Components # ============================================================================= components: securitySchemes: ApiKeyAuth: type: http scheme: bearer bearerFormat: APIKey description: | Phase 0.8b 起,visionA → converter 採 pre-shared API key 認證(1:1 internal trust)。 - Header 格式:`Authorization: Bearer ` - Key 為 64 hex chars(128 bits 熵),由 `openssl rand -hex 32` 產生 - visionA 與 converter 兩端用**完全相同字串**(兩端 env 各自設定) - 不分 read/write scope(API key 即「caller 是 visionA」的完整證明) - 不檢查 issuer / audience / tenant / expiration - Server 端用 `crypto.timingSafeEqual` constant-time compare 防 timing attack 失敗: - 缺 Authorization / 非 Bearer 格式 / token 為空 / key 不符 → 401 `invalid_token` - server `CONVERTER_API_KEY` env 未設定 → 503 `service_unavailable`(fail-secure) 詳見 `docs/autoflow/04-architecture/auth.md` §1 + §4。 > **歷史**:原先設計用 OAuth Bearer JWT(`BearerAuth` + `OAuth2ClientCredentials` > schemes),詳見 visionA repo `ADR-014` / `ADR-015` v2.1。Phase 0.8b 改 API key 後 > 兩個 OAuth scheme 已從本 spec 移除。 parameters: XRequestId: name: X-Request-Id in: header required: false description: | Trace ID。若 client 帶入則 server 沿用;未帶則 server 產 UUIDv4 並回給 client。 schema: type: string format: uuid example: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff JobIdPath: name: id in: path required: true description: Job UUIDv4(建立時 server 生成) schema: type: string format: uuid example: 550e8400-e29b-41d4-a716-446655440000 headers: XRequestId: schema: type: string format: uuid description: 請求對應的 trace ID(與 X-Request-Id request header 相同) XRateLimitLimit: schema: type: integer description: 該 client 在當前 window 的最大允許 request 數(預設 300) XRateLimitRemaining: schema: type: integer description: 該 client 當前 window 還剩多少 request 額度 responses: Unauthorized: description: | API key 不符 / 缺 Authorization header / 非 Bearer 格式 / token 為空。 Phase 0.8b 起,所有 401 一律為 `invalid_token`(沒有 `token_expired` — API key 無過期概念;沒有 `insufficient_scope` / `tenant_mismatch` — API key 不分 scope / tenant)。 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: missing_header: summary: 缺 Authorization header value: error: code: invalid_token message: 缺少或格式錯誤的 Authorization header(需為 Bearer ) request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff wrong_key: summary: API key 不符 value: error: code: invalid_token message: API key 驗證失敗 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff ServiceUnavailable: description: | Server 端 `CONVERTER_API_KEY` env 未設定(fail-secure)。設好 env 重啟即可。 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: api_key_not_configured: value: error: code: service_unavailable message: API key not configured request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff JobNotFound: description: | Job 不存在 — 或存在但屬於不同 client_id(為避免存在性洩露,一律回此) content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: not_found: value: error: code: job_not_found message: Job 不存在 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff RateLimited: description: 超過 per-client_id rate limit headers: Retry-After: schema: type: integer description: 多少秒後可再嘗試 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: rate_limited: value: error: code: rate_limit_exceeded message: 請求頻率過高,請稍後再試 details: retry_after_seconds: 30 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff NotImplemented: description: Phase 2 預留功能,Phase 1 不提供 content: application/json: schema: $ref: '#/components/schemas/ApiError' examples: not_implemented: value: error: code: not_implemented message: 此端點為 Phase 2 預留,尚未實作 request_id: 7c6e4f3b-1a2b-4c3d-9e8f-aabbccddeeff schemas: # ------------------------------------------------------------------------- # 共用:錯誤格式 # ------------------------------------------------------------------------- ApiError: type: object required: [error] properties: error: type: object required: [code, message, request_id] properties: code: type: string description: | 錯誤分類碼(snake_case)。完整清單見 README §Error Codes。 enum: - validation_error - invalid_multipart - invalid_token - job_not_found - not_found - user_has_active_job - job_not_ready_for_promote - source_not_available - file_too_large - invalid_object_key - misconfiguration - storage_unavailable - file_gateway_unavailable - auth_service_unavailable - service_busy - service_unavailable - rate_limit_exceeded - internal_error - not_implemented message: type: string description: 人類可讀的訊息(zh-TW) details: type: object description: 視 code 而定的補充資訊;可能不存在 additionalProperties: true request_id: type: string description: 對應的 X-Request-Id(uuid) format: uuid # ------------------------------------------------------------------------- # /health # ------------------------------------------------------------------------- HealthSnapshot: type: object required: [service, status, timestamp, redis, dependencies] properties: service: type: string enum: [task-scheduler] status: type: string enum: [healthy, degraded, unhealthy] timestamp: type: string format: date-time redis: type: string description: 向後相容欄位(與 dependencies.redis 同值) enum: [connected, disconnected] version: type: string description: 服務版本 example: '1.0.0' dependencies: type: object required: [redis, member_center, file_access_agent] properties: redis: type: string enum: [connected, disconnected] member_center: type: string enum: [reachable, unreachable, pending] file_access_agent: type: string enum: [reachable, unreachable, pending] # ------------------------------------------------------------------------- # POST /jobs request body # ------------------------------------------------------------------------- CreateJobRequest: type: object required: [model, user_id, model_id, version, platform] properties: model: type: string format: binary description: | 原始模型檔(必填)。 - 副檔名只接受 `.onnx` / `.tflite`(PRD §4.4) - 大小 ≤ 500 MB - 不可為空 ref_images[]: type: array description: | 可選的參考圖片陣列(用於 BIE 校正)。 - 0 ~ 100 張 - 每張 ≤ 10 MB items: type: string format: binary user_id: type: string description: | **trust boundary**:由呼叫端決定,server 完全信任。 限制:1-128 字元,`^[A-Za-z0-9._-]+$`,不可含 `..` pattern: '^[A-Za-z0-9._-]{1,128}$' example: alice model_id: type: string description: 數字字串,整數值範圍 1 ~ 65535 example: '1001' version: type: string description: | 版本識別。1-32 字元,`^[A-Za-z0-9._-]+$`(拒含 XSS / 控制字元) pattern: '^[A-Za-z0-9._-]{1,32}$' example: v1.0.0 platform: type: string description: 目標 Kneron 平台 enum: ['520', '720', '530', '630', '730'] enable_evaluate: type: string description: 是否啟用 IP evaluation。`'true'` / `'false'`,缺漏視為 `'false'` enum: ['true', 'false'] default: 'false' enable_sim_fp: type: string description: 是否執行浮點 E2E 模擬。`'true'` / `'false'` enum: ['true', 'false'] default: 'false' enable_sim_fixed: type: string description: 是否執行定點 E2E 模擬。`'true'` / `'false'` enum: ['true', 'false'] default: 'false' enable_sim_hw: type: string description: 是否執行硬體 E2E 模擬。`'true'` / `'false'` enum: ['true', 'false'] default: 'false' metadata: type: string description: | 可選;若有需為合法 JSON object 字串(不可為 array / null / primitive)。 未來擴展用,server 原樣保留。 example: '{"source":"visionA-web"}' CreateJobResponse: type: object required: [job_id, status, stage, progress, created_at, expires_at, user_id] properties: job_id: type: string format: uuid description: server 生成的 UUIDv4 status: type: string enum: [created] description: 建立成功時固定為 `created` stage: type: string enum: [onnx] description: 第一階段固定為 `onnx` progress: type: integer enum: [0] description: 建立成功時固定為 0 created_at: type: string format: date-time expires_at: type: string format: date-time description: 建立後 7 天的時間戳,過期 Redis / MinIO 會清掉相關資料 user_id: type: string # ------------------------------------------------------------------------- # Job 完整 schema(GET / list 共用) # ------------------------------------------------------------------------- Job: type: object required: - job_id - user_id - status - stage - progress - stage_progress - created_at - updated_at - expires_at - stage_timings - input - result_object_keys - error - parameters - metadata properties: job_id: type: string format: uuid user_id: type: string nullable: true status: type: string enum: [created, running, completed, failed] description: | 對外狀態: - `created`:剛建立,第一階段尚未開工 - `running`:某個 stage 進行中(看 `stage` 欄位) - `completed`:全部完成,`result_object_keys` 有值 - `failed`:某階段失敗,`error` 有值 stage: type: string nullable: true enum: [onnx, bie, nef, null] description: | 當前進行中的 stage。`completed` 時為 `null`; `failed` 時為失敗發生時的 stage。 progress: type: integer minimum: 0 maximum: 100 description: 整體 pipeline 進度 0-100 stage_progress: type: integer minimum: 0 maximum: 100 description: 當前 stage 內的進度(worker 上報;Phase 1 多為 0 或 100) created_at: type: string format: date-time updated_at: type: string format: date-time expires_at: type: string format: date-time stage_timings: $ref: '#/components/schemas/StageTimings' input: type: object nullable: true required: [filename, object_key, size_bytes, ref_images_count] properties: filename: type: string nullable: true description: 上傳當下的原始檔名(已 sanitize) object_key: type: string nullable: true description: Converter Bucket 內的物件 key(內部使用) size_bytes: type: integer nullable: true description: 模型檔大小(bytes) ref_images_count: type: integer minimum: 0 description: 參考圖片數量 result_object_keys: type: object nullable: true description: | 僅 `status=completed` 時有值。各 stage 結果檔在 Converter Bucket 內的 object key(後續 promote 用)。 properties: onnx: type: string bie: type: string nef: type: string error: type: object nullable: true description: 僅 `status=failed` 時有值 properties: stage: type: string enum: [onnx, bie, nef] code: type: string description: Worker 端的錯誤碼(如 `quantization_failed`) message: type: string parameters: type: object required: [model_id, version, platform] properties: model_id: type: integer minimum: 1 maximum: 65535 version: type: string platform: type: string enum: ['520', '720', '530', '630', '730'] enable_evaluate: type: boolean enable_sim_fp: type: boolean enable_sim_fixed: type: boolean enable_sim_hw: type: boolean metadata: type: object additionalProperties: true description: client 提交時帶的 metadata,server 原樣保留 StageTimings: type: object description: | 每個 stage 的開始與完成時間。 **Phase 1 限制**:`started_at` 是「Scheduler enqueue 該 stage 的時間」, 非 worker 真的拿起任務的時間。worker 等待 queue 的時間會被算入。 Phase 2 若需精確區分,由 worker 上報 `worker_started_at`。 required: [onnx, bie, nef] properties: onnx: $ref: '#/components/schemas/StageTiming' bie: $ref: '#/components/schemas/StageTiming' nef: $ref: '#/components/schemas/StageTiming' StageTiming: type: object nullable: true properties: started_at: type: string format: date-time nullable: true completed_at: type: string format: date-time nullable: true # ------------------------------------------------------------------------- # GET /jobs (list) # ------------------------------------------------------------------------- ListJobsResponse: type: object required: [jobs, total, next_cursor] properties: jobs: type: array items: $ref: '#/components/schemas/Job' total: type: integer minimum: 0 description: 過濾條件下的總筆數(不只是當頁) next_cursor: type: string nullable: true description: | 下一頁的 opaque cursor(base64-url)。null 表示已是最後一頁。 # ------------------------------------------------------------------------- # POST /jobs/:id/promote # ------------------------------------------------------------------------- PromoteRequest: type: object required: [targets] properties: targets: type: array minItems: 1 maxItems: 10 description: | 要 promote 的清單。每個 source 在同一個 request 中只能出現一次(重複會 400)。 items: $ref: '#/components/schemas/PromoteTarget' PromoteTarget: type: object required: [source, target_object_key] properties: source: type: string enum: [onnx, bie, nef] description: 要從哪個 stage 結果取 target_object_key: type: string maxLength: 1024 description: | File Access Agent 端的目標 key。caller(visionA)決定命名規則。 禁止字元(會回 422 invalid_object_key): - 空字串 - 開頭 `/` - `..`(path traversal) - 反斜線 `\\`(Windows path) - 控制字元 / null byte - `?` `#` `%`(URL 結構字元 / 雙重編碼攻擊) - 長度 > 1024 example: visionA/models/alice/m-1001/v1.0.0/out.nef PromoteResponse: type: object required: [job_id, promoted] properties: job_id: type: string format: uuid promoted: type: array items: type: object required: [source, target_object_key, size_bytes, file_access_agent_etag, promoted_at] properties: source: type: string enum: [onnx, bie, nef] target_object_key: type: string size_bytes: type: integer description: PUT 到 FAA 的 bytes 數 file_access_agent_etag: type: string nullable: true description: FAA 回的 ETag(若有) promoted_at: type: string format: date-time