jim800121chen d8a9517c9d feat(task-scheduler): Phase 0.8b — API key auth + /result endpoint
Auth pillar 從 OAuth 2.0 resource server 改成 pre-shared API key
(visionA ↔ converter 1:1 internal trust)。新增 GET /api/v1/jobs/:id/result
streaming endpoint 給 visionA backend 中轉 NEF 下載。

Phase A(auth 切換):
- 新增 apiKeyMiddleware(constant-time compare、tokenFingerprint、4 audit events)
- 砍 OAuth middleware + JWKS(保留 oauthClient 供 promote → FAA 使用)
- 4 個 endpoint 換掛 requireApiKey
- 加 TRUST_PROXY env + Express trust proxy 設定(forensic source_ip)

Phase B(/result endpoint):
- streaming NEF download with 5min timeout + concurrent cap 10
- Two-tier rate limit(burst 5/10s + sustained 20/min)
- Bandwidth quota(1 GB/hr + 6 GB/24hr)by token_fingerprint
- Range header silently ignored + Accept-Ranges: none
- filename quote-escape + RFC 5987 fallback + sanitize
- 8 個 /result audit events(forensic 完整)

設計演進記錄:docs/TODO-visionA-integration-v2.md(5/2 OAuth → 5/16 API key
→ 5/16 download via converter;對應 visionA repo ADR-015/016)

Tests: 597 → 666 (+69)、29 suites all pass
Security: APPROVE WITH CONDITIONS(單 instance 部署、6 新 env、24hr 監控)
npm audit: 3 vuln → 0(transitive AWS SDK xml chain)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:47:28 +08:00

1801 lines
66 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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-01TDD §1.4.2
# 列了 6 種但 PRD 才是對 user-facing 的合約 — doc-review m6 已記錄)
# 6. ref_image per-file 上限 10MBSec 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 對外 APIPhase 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_API_KEY>`。
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 logPhase 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 credentialAPI key 也無 user 概念),而是 multipart form
fieldPOST或 query stringGET。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 instanceper-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 都需 ApiKeyAuthBearer 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 MBmulter `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. 先寫 MinIOinput + 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 <bearer-token>" \
-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 <bearer-token>" \
-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: 列出 jobRecovery / 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/"<sha256-of-updated_at>"`weak ETag
- client 後續 polling 帶 `If-None-Match: <prev etag>` →
若 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 ETaghash(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 AgentNAS 模型庫)。
## 冪等
- 若 job 已 promoted 過 → 直接回 200 + 既有 promoted_object_keys
(不重打 FAA、不重新讀 MinIO
- 同 `target_object_key` 多次 PUTFAA 端會覆蓋client 安全重試)
## 序列執行
多 target 在 server 端**序列**處理(避免對 FAA 並發壓力與 OOM
## 重試策略
FAA 5xx / network timeoutserver 端內部已重試最多 2 次500ms / 2s
指數退避。client 不需要再重試 502 之外的 case。
FAA 401server 自動 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}/resultstreaming 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 limit5 req / 10s429 + `limit_type: burst`
- Sustained rate limit20 req / 1min429 + `limit_type: sustained`
- Hourly bandwidth quota1 GB / hr429 + `limit_type: bandwidth_hourly`
- Daily bandwidth quota6 GB / 24hr429 + `limit_type: bandwidth_daily`
- Concurrent stream cap10 同時 stream / instance503 + `Retry-After: 30`
- Stream response timeout5 分鐘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="<source_filename_stem>_<platform>.nef"; filename*=UTF-8''<encoded>`
- `filename`ASCII-safe + quote-escape
- `filename*`RFC 5987 extended為未來 unicode 預留)
- 缺 source_filename 或 platform → fallback `job_<jobId>.nef`
Accept-Ranges:
schema:
type: string
example: none
description: '明示不支援 Range requestRFC 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 <CONVERTER_API_KEY>`
- Key 為 64 hex chars128 bits 熵),由 `openssl rand -hex 32` 產生
- visionA 與 converter 兩端用**完全相同字串**(兩端 env 各自設定)
- 不分 read/write scopeAPI 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 <token>
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-Iduuid
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 完整 schemaGET / 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 提交時帶的 metadataserver 原樣保留
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 cursorbase64-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。callervisionA決定命名規則。
禁止字元(會回 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