Skip to content

feat(volc): add native Fire Ark channel - #6653

Open
ENCHIGO wants to merge 1 commit into
QuantumNous:mainfrom
ENCHIGO:feat/volc-native-adapter-upstream
Open

feat(volc): add native Fire Ark channel#6653
ENCHIGO wants to merge 1 commit into
QuantumNous:mainfrom
ENCHIGO:feat/volc-native-adapter-upstream

Conversation

@ENCHIGO

@ENCHIGO ENCHIGO commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

新增独立的 Volc Native 渠道,用于火山方舟 /api/v3/* 原生接口:图片生成与异步内容生成任务。该渠道只会匹配原生路由,不能被 /v1 OpenAI 兼容请求选中;请求体保留原样透传,任务查询、取消与轮询都使用服务端保存的渠道凭据。

此实现由 AI 协助完成,已基于最新 main 的当前适配器接口重新适配并完成本地验证。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 不适用;本 PR 为新功能并关联 Feature Issue。
  • 变更理解: 新渠道与现有 VolcEngine OpenAI 兼容渠道隔离,避免错误路由或跨用户任务访问。
  • 范围聚焦: 本 PR 仅包含火山方舟原生渠道、路由、任务适配与对应测试。
  • 本地验证: 已运行并通过下方列出的测试与构建。
  • 安全合规: 未包含凭据;任务查询与取消仅使用本地归属记录。

📸 运行证明 / Proof of Work

  • go test ./...
  • bun run typecheck
  • bunx oxlint -c .oxlintrc.json src/features/channels/constants.ts src/features/channels/lib/channel-type-config.ts src/features/channels/lib/channel-utils.ts
  • bun run build

Summary by CodeRabbit

  • New Features
    • Added Volcengine (Fire Ark) as a supported native API channel with native image generation capabilities
    • Introduced asynchronous content generation task management with create, list, retrieve, and delete operations
    • Extended configuration interface to support Volcengine channels with API endpoint and credential setup

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This 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.

Changes

Volc Native support

Layer / File(s) Summary
Channel registration and configuration
constant/..., common/api_type.go, web/src/features/channels/...
Registers channel and API type 61, the Fire Ark endpoint, display metadata, frontend configuration, and icon mapping.
Native relay adaptors
relay/channel/volcnative/..., relay/channel/task/volcnative/..., relay/relay_adaptor.go
Adds native request validation and forwarding, response ID substitution, model lists, explicit unsupported-operation errors, and adaptor selection.
Route selection and endpoint wiring
middleware/..., model/..., router/...
Restricts channel selection to native image and content-generation paths and adds image and task routes with middleware.
Image and task lifecycle handling
controller/volc_native.go, service/task_polling.go, tests
Adds image relay, task retrieval, listing, cancellation, response conversion, settlement handling, and Volc Native polling key selection.

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
Loading

Possibly related PRs

Suggested reviewers: calcium-ion

Poem

A rabbit sends native requests through the gate,
Fire Ark tasks now carry their state.
IDs stay hidden, responses shine,
Routes keep each endpoint in line.
Hop, hop—the new channel is live!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements native routing, byte-preserving relay, task operations, isolation, and settlement, but provides no evidence of tiered billing or seven Seedance pricing expressions [#4705]. Add and verify resolution/duration-based tiered billing and the seven Doubao Seedance pricing expressions required by issue #4705.
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the addition of a native Fire Ark/Volc channel, which is the primary change.
Out of Scope Changes check ✅ Passed All listed changes support the new isolated Volc Native channel, its native routes, task handling, relay integration, and UI configuration; no unrelated code is evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
controller/volc_native.go (1)

236-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the "cancelled" marker into a named constant.

The literal "cancelled" is written to task.FailReason at Line 239, is compared in volcNativeTaskStatus at Line 325, and is passed to service.RefundTaskQuota at 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 win

Add a case for a cancelled task that still holds a stored running payload.

Both tests cover the id replacement and the synthesized payload. Neither covers a task whose local status is terminal while task.Data holds an older upstream status. That is the gap flagged on buildVolcNativeTaskResponse in controller/volc_native.go. Add a case with Data set to {"id":"upstream-task-id","status":"running"}, Status: model.TaskStatusFailure, and FailReason: "cancelled", then assert the response status is 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_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

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab0202 and 362c9d6.

📒 Files selected for processing (20)
  • common/api_type.go
  • constant/api_type.go
  • constant/channel.go
  • controller/channel-test.go
  • controller/volc_native.go
  • controller/volc_native_test.go
  • middleware/distributor.go
  • middleware/volc_native_test.go
  • model/ability.go
  • model/channel_cache.go
  • relay/channel/task/volcnative/adaptor.go
  • relay/channel/task/volcnative/adaptor_test.go
  • relay/channel/volcnative/adaptor.go
  • relay/relay_adaptor.go
  • router/relay-router.go
  • router/video-router.go
  • service/task_polling.go
  • web/src/features/channels/constants.ts
  • web/src/features/channels/lib/channel-type-config.ts
  • web/src/features/channels/lib/channel-utils.ts

Comment thread controller/volc_native.go
Comment on lines +194 to +198
key, _, keyErr := channel.GetNextEnabledKey()
if keyErr != nil {
respondVolcNativeError(c, keyErr.StatusCode, "channel_no_available_key", "no upstream credential is available")
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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

Comment thread controller/volc_native.go
Comment on lines +294 to +311
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment thread middleware/distributor.go
Comment thread relay/channel/task/volcnative/adaptor.go
Comment thread web/src/features/channels/constants.ts
This was referenced Aug 5, 2026
@ENCHIGO

ENCHIGO commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

方便时看一下这个 PR?它实现的是 #4705(2026-05 提出,目前仍 open,已有几位用户跟帖等待)。

补充一点背景,说明为什么是新建渠道类型而不是扩展现有的 VolcEngine (45):火山方舟的原生 /api/v3 与 OpenAI 兼容的 /v1 是两套语义。原生接口的请求体带有 sequential_image_generationwatermark 这类厂商特有字段,走 OpenAI 转换路径会被丢弃;异步任务的响应结构也不同。所以 Volc Native (61) 被刻意限制为只服务 /api/v3/images/generations/api/v3/contents/generations/tasks,绝不会被 /v1 请求选中,现有 VolcEngine 渠道的行为完全不受影响。

任务的查询、列表、取消都限定在当前用户自己的任务上,上游 task id 保存在 Task.PrivateData 中、不回传给调用方,避免共享上游凭据时一个用户枚举到另一个用户的任务。这几点都有对应的单测覆盖。

目前无冲突(mergeable: true),可以直接合。如果 20 个文件一次评审偏大,我可以拆成「渠道类型注册 + 路由隔离」和「任务适配器」两个 PR,或者补充任何需要的说明。麻烦了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 新增独立渠道支持火山方舟 /api/v3/* 原生接口透传

1 participant