# Converter Integration Contract > 本文件定義 **visionA-backend 呼叫 kneron_model_converter 的 API 契約**。 > 目的:提前把介面定義清楚,讓 converter 團隊知道要實作什麼;同時讓 visionA-backend 雛形可先用 stub 開發。 --- ## 1. 通訊方向 ``` visionA-backend/api-server kneron_model_converter │ │ │ 1. POST /v1/jobs(提交轉檔) │ │ ────────────────────────────────────►│ │ │ │ ◄────── 202 + {job_id} ──────────────│ │ │ │ 2. GET /v1/jobs/{id}(輪詢 / 或等 webhook) │ ────────────────────────────────────►│ │ ◄────── 200 + {status, result_url}──│ │ │ │ 3. 下載產物(GET result_url) │ │ ────────────────────────────────────►│ ``` Converter 可選: - **Pull 模式**:visionA 輪詢 `/v1/jobs/{id}` - **Push 模式**:visionA 提供 webhook URL,converter 完成後回呼 雛形先用 **Pull 模式**;Phase 1 評估 webhook。 --- ## 2. 認證 **visionA → converter**: - 服務對服務,使用 API Key 或 mTLS - Header:`Authorization: Bearer ` API Key 由 converter 團隊簽發,放 `VISIONA_CONVERTER_API_KEY` env。 --- ## 3. 端點 ### 3.1 POST `/v1/jobs` — 提交轉檔 **Request**: ```json POST /v1/jobs Authorization: Bearer ... Content-Type: application/json { "source": { "type": "url", "url": "https://storage.visiona.cloud/converter/source/demo-user/job-xxx.onnx?signature=...", "checksum_sha256": "abc123...", "format": "onnx" }, "target": { "chip": "kl520", "quantization": "int8", "input_shape": [1, 3, 224, 224] }, "callback": { "webhook_url": null, // 雛形先 null "idempotency_key": "" }, "client_job_id": "" } ``` **欄位說明**: | 欄位 | 必要 | 說明 | |------|-----|------| | `source.type` | ✓ | `"url"` \| `"upload"`(雛形只支援 url) | | `source.url` | ✓ | Presigned GET URL,converter 自己下載 | | `source.checksum_sha256` | ✓ | visionA 計算好的 sha256,converter 下載後驗證 | | `source.format` | ✓ | `"onnx"` \| `"keras"` \| `"tflite"` \| ... | | `target.chip` | ✓ | `"kl520"` \| `"kl720"` | | `target.quantization` | — | `"int8"` \| `"fp16"`,預設依 chip | | `target.input_shape` | — | 若 source 不含 shape,由此補 | | `callback.webhook_url` | — | 未來 push 模式 | | `callback.idempotency_key` | ✓ | 重試時避免重複執行 | | `client_job_id` | ✓ | visionA 內部 job id,converter 回傳時要帶上 | **Response 202**: ```json { "success": true, "data": { "job_id": "cvt-abc-123", // converter 側的 id "status": "queued", "accepted_at": "2026-04-21T12:00:00Z", "estimated_duration_seconds": 120 } } ``` **Response 錯誤**: ```json { "success": false, "error": { "code": "UNSUPPORTED_FORMAT" | "INVALID_CHECKSUM" | "SOURCE_UNREACHABLE" | "QUOTA_EXCEEDED", "message": "..." } } ``` --- ### 3.2 GET `/v1/jobs/{job_id}` — 查詢狀態 **Response 200**: ```json { "success": true, "data": { "job_id": "cvt-abc-123", "client_job_id": "", "status": "queued" | "running" | "succeeded" | "failed", "progress": 0.65, // 0.0 - 1.0 "stage": "quantizing", // 可選 "accepted_at": "...", "started_at": "...", "completed_at": "...", "result": { // status == succeeded 時才有 "url": "https://converter.cloud/result/....nef?signature=...", "url_expires_at": "...", "checksum_sha256": "...", "size_bytes": 12345678, "target_chip": "kl520" }, "error": { // status == failed 時才有 "code": "QUANTIZATION_FAILED", "message": "Layer ... not supported", "details": {} } } } ``` --- ### 3.3 POST `/v1/jobs/{job_id}/cancel` — 取消 **Response**: ```json { "success": true, "data": { "job_id": "...", "status": "cancelled" } } ``` 若已 completed → `400 ALREADY_COMPLETED`;若 already cancelled → `200` idempotent。 --- ### 3.4 Webhook(未來) Converter push 到 visionA 的 webhook URL: **Request(converter → visionA)**: ```json POST https://api.visiona.cloud/webhooks/converter X-Converter-Signature: sha256= Content-Type: application/json { "event": "job.completed" | "job.failed", "job_id": "cvt-abc-123", "client_job_id": "", "status": "succeeded" | "failed", "result": { ... }, "error": { ... }, "timestamp": "..." } ``` **visionA 驗證 signature 後**: - 更新 `converter_jobs` 表 - 若 succeeded,下載產物存到 `storage/converter/result/` - 建立對應的 `models` record(source=converted, source_job_id=) - 回 200 **retry 約定**: - Webhook 失敗 converter 最多重試 5 次,指數退避 - visionA 必須 idempotent 處理(用 `event + job_id` 當 key) --- ## 4. 雛形階段(visionA 端 stub) ```go // internal/converter/stub.go type StubClient struct { jobs map[string]*Job mu sync.Mutex } func (s *StubClient) SubmitConvert(ctx, req) (string, error) { s.mu.Lock() defer s.mu.Unlock() jobID := "stub-job-" + uuid.NewString() s.jobs[jobID] = &Job{ ID: jobID, Status: "queued", CreatedAt: time.Now(), TargetChip: req.TargetChip, } // 雛形:15 秒後「完成」,指向假 result URL time.AfterFunc(15*time.Second, func() { s.mu.Lock() defer s.mu.Unlock() if j, ok := s.jobs[jobID]; ok { j.Status = "succeeded" j.ResultKey = "stub-result-key" j.CompletedAt = ptrTime(time.Now()) } }) return jobID, nil } ``` 雛形期前端可以用這個 stub 走完整 UX,但不會真的產生 `.nef`。 --- ## 5. Error 對應表 | Converter 回傳 | visionA 前端顯示 | |---------------|----------------| | `UNSUPPORTED_FORMAT` | 「目前不支援此格式」| | `INVALID_CHECKSUM` | 「檔案下載驗證失敗,請重新上傳」| | `QUOTA_EXCEEDED` | 「本月轉檔配額已滿」| | `QUANTIZATION_FAILED` | 「模型轉檔失敗:[detail]」| | 其他 | 「轉檔失敗,請聯絡支援」| --- ## 6. 相容性 / 版本 - URL 含 `/v1/` 前綴,日後升級 `/v2/` 可並存 - 欄位採「新增只是 optional」原則,不破壞舊版 - visionA 用 env `VISIONA_CONVERTER_API_VERSION` 切換(預設 `v1`) --- ## 7. 給 Converter 團隊的確認清單 - [ ] 同意採用此 API spec? - [ ] 確認 source.type `"url"` 的 presigned URL 長度 / TTL 要求 - [ ] 確認支援的 source format 清單 - [ ] 確認 webhook push 模式的實作意願 / 時程 - [ ] 確認 rate limit / quota 政策 - [ ] 確認產物儲存位置(converter 自己的 bucket,還是回 visionA bucket?) - [ ] 提供測試 API key --- **雛形實作**:`internal/converter/stub.go` + 前端走 stub 驗流程。 **Phase 1**:`internal/converter/http.go` 實作上述 API + webhook endpoint。