feat(volc): add native Fire Ark channel - #6653
Conversation
WalkthroughThis PR adds Volc Native channel type 61 support for Fire Ark image and asynchronous content generation APIs. It adds native adaptors, route filtering, controllers, task lifecycle handling, polling key selection, and frontend channel configuration. ChangesVolc Native support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Router
participant Distributor
participant RelayVolcNativeImage
participant FireArkAPI
Client->>Router: POST /api/v3/images/generations
Router->>Distributor: Validate native route and select channel
Distributor->>RelayVolcNativeImage: Dispatch request
RelayVolcNativeImage->>FireArkAPI: Forward validated JSON
FireArkAPI-->>RelayVolcNativeImage: Return generation response
RelayVolcNativeImage-->>Client: Return response with settlement
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
controller/volc_native.go (1)
236-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the
"cancelled"marker into a named constant.The literal
"cancelled"is written totask.FailReasonat Line 239, is compared involcNativeTaskStatusat Line 325, and is passed toservice.RefundTaskQuotaat Line 247. The status mapping depends on the exact string. A named constant prevents a silent mismatch after a later edit.♻️ Proposed refactor
+const volcNativeCancelReason = "cancelled" + previousStatus := task.Status task.Status = model.TaskStatusFailure task.Progress = "100%" - task.FailReason = "cancelled" + task.FailReason = volcNativeCancelReason task.FinishTime = time.Now().Unix() won, err := task.UpdateWithStatus(previousStatus) if err != nil { respondVolcNativeError(c, http.StatusInternalServerError, "task_update_failed", "failed to update local task") return } if won { - service.RefundTaskQuota(c.Request.Context(), task, "cancelled") + service.RefundTaskQuota(c.Request.Context(), task, volcNativeCancelReason)case model.TaskStatusFailure: - if task.FailReason == "cancelled" { + if task.FailReason == volcNativeCancelReason { return "cancelled" }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/volc_native.go` around lines 236 - 247, Extract the literal "cancelled" string into a named constant and replace all three occurrences: where task.FailReason is assigned in the diff block, where it is compared in volcNativeTaskStatus, and where it is passed to service.RefundTaskQuota. Using a named constant ensures that if the status string is updated in one place, all dependent code remains in sync and prevents silent mismatches.controller/volc_native_test.go (1)
11-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a case for a cancelled task that still holds a stored
runningpayload.Both tests cover the id replacement and the synthesized payload. Neither covers a task whose local status is terminal while
task.Dataholds an older upstream status. That is the gap flagged onbuildVolcNativeTaskResponseincontroller/volc_native.go. Add a case withDataset to{"id":"upstream-task-id","status":"running"},Status: model.TaskStatusFailure, andFailReason: "cancelled", then assert the response status iscancelled.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@controller/volc_native_test.go` around lines 11 - 38, Add a new test function following the naming pattern of the existing tests to cover the case where buildVolcNativeTaskResponse receives a task with a terminal local status and stale upstream data. Create a task with Data containing an old running status, Status set to model.TaskStatusFailure, and FailReason set to cancelled, then call buildVolcNativeTaskResponse and assert that the response status field is cancelled rather than the stale running status from the Data field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@controller/volc_native.go`:
- Around line 294-311: Update buildVolcNativeTaskResponse so that when task.Data
already contains a status field, the sjson.SetBytes update overwrites both id
and status, using volcNativeTaskStatus(task) for the local status. Preserve the
existing stored-payload path and fallback behavior.
- Around line 194-198: Update RelayVolcNativeTaskDelete around GetNextEnabledKey
to first use the credential stored in the task’s PrivateData.Key when present,
and only fall back to channel.GetNextEnabledKey() when it is absent. Preserve
the existing no-available-key error handling for cases where neither credential
is available.
In `@middleware/distributor.go`:
- Around line 443-447: Extract the Volc Native path allowlist into a shared
helper function in a common package such as constant. Create a shared slice
containing the two path prefixes ("/api/v3/images/generations" and
"/api/v3/contents/generations/tasks") and a function to check against them. At
middleware/distributor.go#443-447, replace the body of isVolcNativePath to call
the shared helper. At model/ability.go#176-193, replace the inline
strings.HasPrefix checks with a call to the same shared helper. At
model/channel_cache.go#245-249, replace the body of isVolcNativeRequestPath to
call the shared helper or delete the function and invoke the helper directly at
the call site.
In `@relay/channel/task/volcnative/adaptor.go`:
- Around line 79-83: Move the resp.Body.Close registration immediately after the
response nil check and before io.ReadAll in the surrounding request-handling
function, ensuring the body is closed on both successful and failed reads.
Remove the later standalone close while preserving the existing read-error
return through taskError.
In `@web/src/features/channels/constants.ts`:
- Line 84: Replace the Volc Native user-facing label in
web/src/features/channels/constants.ts lines 84-84 with an i18n translation key,
and update the corresponding CHANNEL_TYPE_OPTIONS rendering to resolve it via
t(). In web/src/features/channels/lib/channel-type-config.ts lines 169-175,
replace the direct name and hint strings in ChannelTypeConfig.hints with
translation keys and resolve them through t() at render time; update both sites
consistently without adding hardcoded localized text.
---
Nitpick comments:
In `@controller/volc_native_test.go`:
- Around line 11-38: Add a new test function following the naming pattern of the
existing tests to cover the case where buildVolcNativeTaskResponse receives a
task with a terminal local status and stale upstream data. Create a task with
Data containing an old running status, Status set to model.TaskStatusFailure,
and FailReason set to cancelled, then call buildVolcNativeTaskResponse and
assert that the response status field is cancelled rather than the stale running
status from the Data field.
In `@controller/volc_native.go`:
- Around line 236-247: Extract the literal "cancelled" string into a named
constant and replace all three occurrences: where task.FailReason is assigned in
the diff block, where it is compared in volcNativeTaskStatus, and where it is
passed to service.RefundTaskQuota. Using a named constant ensures that if the
status string is updated in one place, all dependent code remains in sync and
prevents silent mismatches.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: cfb6e264-717e-4d82-b3d0-368a9b0535d0
📒 Files selected for processing (20)
common/api_type.goconstant/api_type.goconstant/channel.gocontroller/channel-test.gocontroller/volc_native.gocontroller/volc_native_test.gomiddleware/distributor.gomiddleware/volc_native_test.gomodel/ability.gomodel/channel_cache.gorelay/channel/task/volcnative/adaptor.gorelay/channel/task/volcnative/adaptor_test.gorelay/channel/volcnative/adaptor.gorelay/relay_adaptor.gorouter/relay-router.gorouter/video-router.goservice/task_polling.goweb/src/features/channels/constants.tsweb/src/features/channels/lib/channel-type-config.tsweb/src/features/channels/lib/channel-utils.ts
| key, _, keyErr := channel.GetNextEnabledKey() | ||
| if keyErr != nil { | ||
| respondVolcNativeError(c, keyErr.StatusCode, "channel_no_available_key", "no upstream credential is available") | ||
| return | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Reuse the credential captured at task submission before falling back to the channel key selector.
RelayVolcNativeTaskDelete derives the credential with channel.GetNextEnabledKey() only. service/task_polling.go prefers task.PrivateData.Key when it is set (lines 468-471). If the channel stores several keys that map to different Fire Ark accounts, the key selector can return a key that does not own the upstream task, and the cancellation fails with an upstream authorization error.
Prefer the key stored on the task, then fall back to the selector.
🔧 Proposed fix
- key, _, keyErr := channel.GetNextEnabledKey()
- if keyErr != nil {
- respondVolcNativeError(c, keyErr.StatusCode, "channel_no_available_key", "no upstream credential is available")
- return
- }
+ key := task.PrivateData.Key
+ if key == "" {
+ selectedKey, _, keyErr := channel.GetNextEnabledKey()
+ if keyErr != nil {
+ respondVolcNativeError(c, keyErr.StatusCode, "channel_no_available_key", "no upstream credential is available")
+ return
+ }
+ key = selectedKey
+ }Based on learnings: "In async video task flows (Sora2/OpenAI) ensure follow-up requests (polling, content download) reuse the authentication context captured at task submission time (API key + header overrides) instead of re-deriving from channel.Key at each call site. Prefer task.PrivateData.Key when available over channel.Key."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| key, _, keyErr := channel.GetNextEnabledKey() | |
| if keyErr != nil { | |
| respondVolcNativeError(c, keyErr.StatusCode, "channel_no_available_key", "no upstream credential is available") | |
| return | |
| } | |
| key := task.PrivateData.Key | |
| if key == "" { | |
| selectedKey, _, keyErr := channel.GetNextEnabledKey() | |
| if keyErr != nil { | |
| respondVolcNativeError(c, keyErr.StatusCode, "channel_no_available_key", "no upstream credential is available") | |
| return | |
| } | |
| key = selectedKey | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@controller/volc_native.go` around lines 194 - 198, Update
RelayVolcNativeTaskDelete around GetNextEnabledKey to first use the credential
stored in the task’s PrivateData.Key when present, and only fall back to
channel.GetNextEnabledKey() when it is absent. Preserve the existing
no-available-key error handling for cases where neither credential is available.
Source: Learnings
| func buildVolcNativeTaskResponse(task *model.Task) []byte { | ||
| if status := gjson.GetBytes(task.Data, "status"); status.Exists() { | ||
| if body, err := sjson.SetBytes(task.Data, "id", task.TaskID); err == nil { | ||
| return body | ||
| } | ||
| } | ||
| body, err := common.Marshal(gin.H{ | ||
| "id": task.TaskID, | ||
| "model": volcNativeTaskModel(task), | ||
| "status": volcNativeTaskStatus(task), | ||
| "created_at": task.CreatedAt, | ||
| "updated_at": task.UpdatedAt, | ||
| }) | ||
| if err != nil { | ||
| return []byte(`{"status":"queued"}`) | ||
| } | ||
| return body | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Override the stored upstream status with the local task status.
When task.Data contains a status field, this function returns the stored upstream payload and replaces only id. The stored payload holds the status from the last poll. RelayVolcNativeTaskDelete calls this function at Line 258 immediately after it sets model.TaskStatusFailure and FailReason = "cancelled", so the response can report "status":"running" for a task the server just cancelled. RelayVolcNativeTaskFetch has the same gap for any task whose local status advanced after the last stored payload.
Set the status field from volcNativeTaskStatus(task) as well as id.
🐛 Proposed fix
func buildVolcNativeTaskResponse(task *model.Task) []byte {
if status := gjson.GetBytes(task.Data, "status"); status.Exists() {
if body, err := sjson.SetBytes(task.Data, "id", task.TaskID); err == nil {
- return body
+ if body, err = sjson.SetBytes(body, "status", volcNativeTaskStatus(task)); err == nil {
+ return body
+ }
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func buildVolcNativeTaskResponse(task *model.Task) []byte { | |
| if status := gjson.GetBytes(task.Data, "status"); status.Exists() { | |
| if body, err := sjson.SetBytes(task.Data, "id", task.TaskID); err == nil { | |
| return body | |
| } | |
| } | |
| body, err := common.Marshal(gin.H{ | |
| "id": task.TaskID, | |
| "model": volcNativeTaskModel(task), | |
| "status": volcNativeTaskStatus(task), | |
| "created_at": task.CreatedAt, | |
| "updated_at": task.UpdatedAt, | |
| }) | |
| if err != nil { | |
| return []byte(`{"status":"queued"}`) | |
| } | |
| return body | |
| } | |
| func buildVolcNativeTaskResponse(task *model.Task) []byte { | |
| if status := gjson.GetBytes(task.Data, "status"); status.Exists() { | |
| if body, err := sjson.SetBytes(task.Data, "id", task.TaskID); err == nil { | |
| if body, err = sjson.SetBytes(body, "status", volcNativeTaskStatus(task)); err == nil { | |
| return body | |
| } | |
| } | |
| } | |
| body, err := common.Marshal(gin.H{ | |
| "id": task.TaskID, | |
| "model": volcNativeTaskModel(task), | |
| "status": volcNativeTaskStatus(task), | |
| "created_at": task.CreatedAt, | |
| "updated_at": task.UpdatedAt, | |
| }) | |
| if err != nil { | |
| return []byte(`{"status":"queued"}`) | |
| } | |
| return body | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@controller/volc_native.go` around lines 294 - 311, Update
buildVolcNativeTaskResponse so that when task.Data already contains a status
field, the sjson.SetBytes update overwrites both id and status, using
volcNativeTaskStatus(task) for the local status. Preserve the existing
stored-payload path and fallback behavior.
|
方便时看一下这个 PR?它实现的是 #4705(2026-05 提出,目前仍 open,已有几位用户跟帖等待)。 补充一点背景,说明为什么是新建渠道类型而不是扩展现有的 VolcEngine (45):火山方舟的原生 任务的查询、列表、取消都限定在当前用户自己的任务上,上游 task id 保存在 目前无冲突( |
Important
📝 变更描述 / Description
新增独立的 Volc Native 渠道,用于火山方舟
/api/v3/*原生接口:图片生成与异步内容生成任务。该渠道只会匹配原生路由,不能被/v1OpenAI 兼容请求选中;请求体保留原样透传,任务查询、取消与轮询都使用服务端保存的渠道凭据。此实现由 AI 协助完成,已基于最新
main的当前适配器接口重新适配并完成本地验证。🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
📸 运行证明 / Proof of Work
go test ./...bun run typecheckbunx oxlint -c .oxlintrc.json src/features/channels/constants.ts src/features/channels/lib/channel-type-config.ts src/features/channels/lib/channel-utils.tsbun run buildSummary by CodeRabbit