Skip to content

feat: 新增 VolcAdapter (58) 渠道——火山方舟原生 API 兼容代理 - #4702

Closed
JAYotta wants to merge 35 commits into
QuantumNous:mainfrom
GraylightCN:main
Closed

feat: 新增 VolcAdapter (58) 渠道——火山方舟原生 API 兼容代理#4702
JAYotta wants to merge 35 commits into
QuantumNous:mainfrom
GraylightCN:main

Conversation

@JAYotta

@JAYotta JAYotta commented May 8, 2026

Copy link
Copy Markdown

概述

新增 ChannelTypeVolcAdapter (channel type 58) 渠道,作为火山方舟(Volc Ark)原生 API 的兼容代理。该渠道与现有 ChannelTypeVolcEngine (45) 共存,不修改 volcengine 渠道的任何现有行为

  • volcengine (45):保持原状,仍走 OpenAI 兼容格式(/v1/chat/completions 等)。
  • volcadapter (58)(本 PR 新增):原生火山方舟格式透传,支持 /api/v3/contents/generations/tasks*(视频/图片/3D 任务)和 /api/v3/images/generations(图片生成)。请求体字节级转发,保留所有火山私有字段(sequential_image_generationoptimize_prompt_optionswatermarksafety_identifier 等)不被解析丢弃。

适用场景:用户使用官方 Volc SDK 或 cURL 直接调用火山方舟 API 时,可以把 baseURL 指到 NewAPI 实例,请求体一字不差地转发到上游。

主要新增功能

1. 新渠道类型 ChannelTypeVolcAdapter (58) + APITypeVolcAdapter

  • constant/channel.go: 新增渠道类型 58 VolcAdapter,base URL 默认指向 https://ark.cn-beijing.volces.com
  • constant/api_type.go + common/api_type.go: 新增 APITypeVolcAdapter,渠道 58 路由到独立的 image adaptor,不复用 volcengine.Adaptor
  • relay/relay_adaptor.go: 注册 APITypeVolcAdapter → volcadapter.Adaptor

2. 独立 image channel.Adaptor

relay/channel/volcadapter/adaptor.go (新建,~100 行):实现完整 channel.Adaptor interface。

  • 仅支持 /api/v3/images/generations,其他 relay mode 返回明确错误 "volcadapter does not support relay mode N; for OpenAI-format requests use the volcengine channel"
  • 实现 ConvertVolcRequest 作为 no-op pass-through(请求体已经是 Volc 原生格式)。
  • 其他 Convert*Request 方法返回 "not supported" 引导用户走 volcengine 渠道。
  • DoResponse 委托给 openai.Adaptor.DoResponse(火山图片响应与 OpenAI 兼容)。
  • GetModelList 复用 task adaptor 的模型列表(同一个上游服务)。

3. Task adaptor

relay/channel/task/volcadapter/ (新建):实现 TaskAdaptor interface 处理异步视频/图片/3D 任务。

  • adaptor.go: 字节级请求体透传(保留全部 Volc 字段),仅在 info.IsModelMapped 时通过 patchVolcBodyModel 改写 model 字段。
  • seedance_estimator.go: 7 个 Seedance 模型的 token 估算(Volc 公式:(input_video_dur + output_dur) × W × H × FPS / 1024),用于 tiered_expr 计费的预扣 token 上限。
  • volc_helpers.go: 字节级 JSON patch 工具(保留未知字段)。
  • constants.go: 模型列表(Doubao Seedance 1.0/1.5/2.0 系列 + lite-i2v/lite-t2v/pro/pro-fast 等 7 个完整 ID)。

4. Native pass-through handlers

Image: relay/volc_handler.go (新建)

VolcImageHelper 处理 /api/v3/images/generations,模式参考 GeminiHelper

  • 解析 dto.VolcImageRequest(仅捕获 size、prompt 等已知字段,body 仍按原样转发)。
  • 类型断言 volcImageConverter interface 检查渠道支持,不支持返回 400。
  • 通过 applyVolcImagePatches helper 在转发前应用 model mapping 和 param override(byte-level JSON patch,不破坏未知字段)。

Task: router/video-router.go + relay/relay_task.go

新增 /api/v3/contents/generations/tasks POST/GET/DELETE 路由:

  • POST: 提交任务(relay_format=volc,走 RelayFormatVolc 分支)。
  • GET: 单任务查询(tasks/:id)和列表查询(tasks?...),返回 Volc 原生 ContentGenerationTask shape。
  • DELETE: controller/volc_task_delete.go 处理任务取消(owner 校验 + 上游 DELETE 转发 + 本地状态更新 + 退款)。

5. Tiered_expr 计费在 volcadapter 上的接入

pkg/billingexpr/(已在上游存在)的 tiered_expr 引擎首次接入 task channel:

  • relay/channel/task/volcadapter/adaptor.go: 实现 EstimateBillingEstimateBillingTokensAdjustBillingOnComplete,使任务在提交时按 param("resolution") == "1080p" ? ... : ... 等表达式计算预扣 quota,完成时按实际 completion_tokens 重算。
  • model/task.go: 新增 TieredVolcFlags 字段(generate_audiodrafthas_video_inputresolutiondurationservice_tier),在提交时从请求体抓取,settle 时通过 param() 暴露给表达式。这在「callback-only 部署」(task.Data 仅含 {"id":"..."} 的场景)下保证表达式能拿到正确参数。
  • controller/relay.go: 新增 extractVolcFlags + parseVolcDuration,支持 float64/int/int64/json.Number/string 多种 duration 形态,拒绝非整数和负数(避免 15.9 被静默截断成 15)。

6. 异步任务结算(settle)链路重构

service/task_billing.go + service/task_polling.go 重构异步任务的差额结算:

  • 单事务原子提交:tasks.quota UPDATE + 钱包/订阅扣款 + users.used_quota / channels.used_quota 更新 + billing log 一次性提交,避免「task UPDATE 成功但钱包写入失败」的不一致。
  • RowsAffected 守卫:targeted UPDATE 匹配 0 行(task ID 错误或行被删)时立刻 abort,不应用任何副作用。
  • SettleTaskBillingOnComplete 导出,接受 nil adaptor(callback handler 在 init 顺序未跑完时仍可结算,走 token fallback 路径)。
  • effectiveTokenCount helper:优先 TotalTokens,回退 CompletionTokens(部分火山响应只填 completion 一侧)。
  • CAS 守卫:task.UpdateWithStatus(snap.Status) 返回 won 标志,polling 和 callback 两条路径都检查,防止双重结算。
  • 缓存失效:事务提交后 model.InvalidateUserCache(task.UserId) 确保 Redis 用户余额不滞后。

7. 计费表达式编辑器 UI

web/classic/src/pages/Setting/Ratio/components/(已有的分档计费编辑器)增强:

  • TieredPricingEditor.jsx: 加 7 个 Doubao Seedance 模型预设(PRESET_GROUPS list)。
  • requestRuleExpr.js: hoist regex 到 module 作用域(性能);has() 表达式安全 parse 带引号值(避免输入 \q 之类崩溃);用双引号代替单引号修复 gjson 路径匹配(Volc 视频字段如 content.#(type=="video_url"))。
  • requestRuleExpr.test.js: 新增针对 has() 安全 parse 的测试。

改动文件清单

constant/channel.go                          +3       新增 ChannelTypeVolcAdapter
constant/api_type.go                         +1       新增 APITypeVolcAdapter
common/api_type.go                           +2       渠道 → API type 路由
relay/relay_adaptor.go                       +6       API type → adaptor 路由
relay/channel/volcadapter/adaptor.go         +102     image channel.Adaptor (新建)
relay/channel/task/volcadapter/              +1422    task adaptor + 估算器 + helpers + 测试 (新建)
controller/volc_task_delete.go               +192     DELETE 任务 handler (新建)
controller/relay.go                          +137     extractVolcFlags / parseVolcDuration
controller/relay_volc_flags_test.go          +59      Volc flags 解析测试
relay/volc_handler.go                        +178     VolcImageHelper (新建)
relay/volc_handler_test.go                   +384     image handler 测试
relay/relay_task.go                          +257     RelayFormatVolc 分支 + 列表/单任务查询
router/video-router.go                       +36      /api/v3/contents/generations/tasks 路由
router/relay-router.go                       +14      /api/v3/images/generations 路由
service/task_billing.go                      +124     事务化 settle
service/task_billing_test.go                 +423     settle 回归测试
service/task_polling.go                      +54      Settle 导出 + nil adaptor 支持
model/task.go                                +34      TieredVolcFlags
dto/volc_image_request.go                    +79      Volc image DTO (新建)
types/relay_format.go                        +7       RelayFormatVolc
web/classic/src/pages/Setting/Ratio/...      +298     UI 预设 + 表达式工具改进
relay/helper/price.go                        +83      tiered_expr 接入
relay/helper/valid_request.go                +18      RelayFormatVolc 请求验证
middleware/distributor.go                    +15      ChannelTypeVolcAdapter 路由信息
common/endpoint_type.go                      +12      新增 endpoint type
common/endpoint_defaults.go                  +2
constant/endpoint_type.go                    +2
... (其他小文件 / 测试)

总计 56 文件,+5685/-88 行(其中测试约 3000 行)。

测试覆盖

  • relay/channel/task/volcadapter/: 16 个测试(adaptor、estimator、Seedance presets、constants)
  • relay/volc_handler_test.go: 8 个测试(body pass-through、ConvertVolcRequest 支持检测、model mapping、param override)
  • controller/relay_volc_flags_test.go: 3 个测试(duration 多种形态解析、非法输入拒绝)
  • controller/volc_task_delete_test.go: 4 个测试
  • service/task_billing_test.go: 19 个测试(事务结算、CAS、token fallback、PerCallBilling 跳过、各种回滚场景)
  • relay/relay_task_volc_list_test.go: list 端点构造测试
  • relay/volc_task_test.go: 完整 RelayTask 流程
  • common/endpoint_type_test.goconstant/channel_test.gorelay/relay_adaptor_test.go: 注册一致性测试
  • middleware/distributor_test.go: routing 测试
  • web/classic/src/pages/Setting/Ratio/components/requestRuleExpr.test.js: 编辑器表达式工具测试

兼容性

  • 不修改 relay/channel/volcengine/*:现有 volcengine 渠道(45)行为完全不变。
  • ✅ 新增 APITypeVolcAdapter 不影响其他 API type 的路由。
  • ✅ Tiered_expr 是 opt-in:模型未配置 BillingMode = tiered_expr 时仍走原来的 ratio 计费。
  • ✅ Settle 重构对 ratio 计费完全透明(事务化只是把多步原子化)。
  • ✅ Volc native body 是字节级透传,未来火山新增字段无需 NewAPI 适配。
  • ✅ 数据库新字段(TieredVolcFlags)作为 task.PrivateData.BillingContext 的子结构以 JSON 存储,不影响 schema migration。

配置说明

要使用 volcadapter 渠道:

  1. 管理后台「渠道」新建渠道,类型选 VolcAdapter (58)。
  2. base URL 默认 https://ark.cn-beijing.volces.com,可改。
  3. API key 填火山方舟的 ARK API Key。
  4. 模型列表填 doubao-seedance-2-0-260128 等 ID(参考 relay/channel/task/volcadapter/constants.go)。
  5. (可选)「分档计费」面板加载 Seedance 预设,按需调整。

请求示例(视频生成):

curl -X POST https://your-newapi/api/v3/contents/generations/tasks \
  -H "Authorization: Bearer sk-xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao-seedance-2-0-260128",
    "content": [
      {"type": "text", "text": "一只猫在草原上奔跑"}
    ],
    "ratio": "16:9",
    "resolution": "1080p",
    "duration": 5,
    "watermark": false
  }'

Summary by CodeRabbit

Release Notes

  • New Features

    • Added VolcAdapter channel (channel 58) with support for Seedream image generation and Seedance video generation models.
    • Introduced Volc Ark-compatible endpoints: image generation (POST /api/v3/images/generations) and video task management (POST/GET/DELETE /api/v3/contents/generations/tasks, GET /api/v3/contents/generations/tasks/:id).
    • Implemented tiered billing expressions for video generation tasks with token-based estimation.
  • Improvements

    • Enhanced task quota settlement with atomic database transactions and improved accounting accuracy.
    • Refined billing calculations and token estimation logic for better pricing accuracy.

JAYotta and others added 30 commits May 8, 2026 15:39
Add Volc task list route adaptation and explicit unsupported DELETE behavior, preserve Ark video payload fields, and update official seedream/seedance model IDs with focused test coverage.

Made-with: Cursor
…dpoint types

Task 1 – Move image route to relay-router.go:
- Register /volc/api/v3 group in relay-router.go with only POST /images/generations
  (synchronous relay via controller.Relay).
- Remove POST /images/generations from video-router.go volcV3Router (async task group).
- Drop now-unused `types` import from video-router.go.

Task 2 – Trim volcengine/constants.go:
- Remove doubao-seedance-* and bare seedance-* video model entries from
  relay/channel/volcengine/constants.go (wrong channel — they belong on doubao-video).
- Keep all doubao-seedream-* and bare seedream-* image entries plus LLM entries.
- Move seedance-* bare aliases to relay/channel/task/doubao/constants.go and add all
  official date-suffixed IDs from the 2026-04-27 Ark API scan.

Task 3 – Rewrite middleware/volc_adapter_test.go:
- Extract runVolcMiddlewareCase helper to eliminate boilerplate.
- Replace veo-1 with realistic doubao-seedance-2-0-260128 model.
- Add TestVolcConvert_ImageGeneration_T2I, _I2I (doubao-seedream models, image array).
- Add TestVolcConvert_VideoSubmit_T2V (action set), _I2V (action absent when image present).
- Improve VideoFetchByID, VideoList, VideoDelete tests with full assertions.
- Add TestVolcConvert_RequestKeyFallback: table-driven model/prompt fallback chain
  (model → model_name → req_key, prompt → content).
- Add TestVolcConvert_InvalidBody: invalid JSON and empty body → 400.
- Assert KeyRequestBody context key via deep-equal on all submit/image cases.
- Assert metadata deep-equals the entire original request body (not spot-checked).

Task 4 – Add volc endpoint types for pricing/marketplace UI:
- Add EndpointTypeVolcImage = "volc-image" and EndpointTypeVolcVideo = "volc-video"
  to constant/endpoint_type.go.
- Register default paths in common/endpoint_defaults.go:
  volc-image → POST /volc/api/v3/images/generations
  volc-video → POST /volc/api/v3/contents/generations/tasks
- Add "seedream" to common/model.go ImageGenerationModels so IsImageGenerationModel
  matches doubao-seedream-* and seedream-* names.
- Update GetEndpointTypesByChannelType in common/endpoint_type.go:
  ChannelTypeDoubaoVideo → [volc-video, openai-video]
  ChannelTypeVolcEngine + image model → [volc-image, image-generation, openai]
  ChannelTypeVolcEngine + LLM/other → fall through to default (openai)
- Pricing API wiring verified: supportedEndpointMap is auto-populated via
  GetDefaultEndpointInfo; no controller changes needed.
- i18n: PricingEndpointTypes.jsx uses raw endpointType string as label
  (getEndpointTypeLabel returns endpointType verbatim); no translation keys exist
  for any endpoint type in the JSON locale files, so "volc-image"/"volc-video"
  will display as raw strings — consistent with all other endpoint types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Volc's official SDKs default to base URL https://ark.cn-beijing.volces.com
and request /api/v3/...; mounting our compat gateway at the same path means
users only need to change the domain. There's no collision with existing
/api/* admin routes (those are all specific paths, none under /api/v3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Splits the volc-compat API endpoints onto a dedicated channel type so
ChannelTypeVolcEngine (45) and ChannelTypeDoubaoVideo (54) revert to
their pre-cleanup minimal scope (LLM/TTS and OpenAI-format video tasks
respectively). The new VolcAdapter channel reuses existing volcengine
and taskdoubao adaptors but ships its own default model list (seedream
+ seedance) and binds the volc-image/volc-video endpoint types.

Display name chosen: "VolcAdapter" (matches the naming style of existing
single-word entries like "VolcEngine", "DoubaoVideo", etc.)

Channel type number: 58 — verified as next available after ChannelTypeCodex=57.
ChannelTypeDummy becomes 58 (same value, per Go const repeat semantics).

Changes by task:
- Task 1.1: constant/channel.go — add ChannelTypeVolcAdapter=58, base URL,
  display name; common/api_type.go — map to APITypeVolcEngine
- Task 1.2: relay/relay_adaptor.go — add VolcAdapter to taskdoubao case;
  relay/common/relay_info.go — add to streamSupportedChannels
- Task 1.3: relay/channel/volcadapter/constants.go — new package with
  seedream + seedance model list; controller/model.go — register in
  openAIModels and override channelId2Models entry
- Task 1.4: common/endpoint_type.go — remove VolcEngine/DoubaoVideo volc-*
  endpoint bindings; add ChannelTypeVolcAdapter case with per-model dispatch
- Task 1.5: relay/channel/volcengine/constants.go — revert to pre-worktree
  state (LLM + seedream + seedance + seed-thinking);
  relay/channel/task/doubao/constants.go — revert (7 official + 2 legacy,
  no bare date aliases)
- Task 1.6: relay/relay_task.go — change list filter to VolcAdapter platform;
  relay/relay_task_volc_list_test.go — update platform + add coexistence test
- Task 1.7: middleware unchanged (no channel-type refs)
- Task 1.8: web/src/constants/channel.constants.js — add type 58 to
  CHANNEL_OPTIONS; web/src/helpers/render.jsx — add case 58 icon (Doubao)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Now that the volc-compat gateway lives on its own ChannelTypeVolcAdapter
channel, the existing VolcEngine (45) and DoubaoVideo (54) channels no
longer need to advertise the full set of seedream/seedance models. Match
upstream/main's minimal model lists so this branch stays close to the
upstream merge base — the new-and-shiny seedream/seedance entries are
available on the dedicated VolcAdapter channel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds RelayFormatVolc and routes the volc-compat image endpoint
through a dedicated handler (mirroring the Gemini pattern) instead
of cursor's body-rewriting middleware. Volc body fields (size 2K/4K,
sequential_image_generation, optimize_prompt_options, watermark, etc.)
flow through to upstream without translation.

Changes:
- types/relay_format.go: add RelayFormatVolc constant
- dto/volc_image_request.go: new DTO that captures known Volc fields
  plus an Extra map for unknown fields (preserves byte-identity)
- relay/channel/adapter.go: add ConvertVolcRequest to Adaptor interface
- relay/channel/volcengine/adaptor.go: no-op pass-through implementation
- relay/channel/*/adaptor.go (31 files): "unsupported" stub implementations
- relay/helper/valid_request.go: add RelayFormatVolc case + validator
- relay/volc_handler.go: new VolcImageHelper (mirrors GeminiHelper)
- relay/common/relay_info.go: GenRelayInfoVolc + GenRelayInfo case
- controller/relay.go: volcRelayHandler + RelayFormatVolc switch case
- router/relay-router.go: image route uses RelayFormatVolc, removes
  VolcRequestConvert() middleware from the image route group

The task path still uses the existing middleware in this commit;
the next commit migrates it and deletes the middleware entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrates the volc-compat task endpoints (submit, fetch, list, delete)
to native RelayFormatVolc pass-through, matching the image side from
the prior commit. Body bytes flow byte-identical to upstream.

Deletes middleware/volc_adapter.go and its rewritten test now that
both image and task paths bypass the middleware.

Key changes:
- controller/relay.go: add RelayTaskVolcSubmit (GenRelayInfo with
  RelayFormatVolc), RelayTaskFetchVolc, RelayTaskVolcDelete (501)
- relay/common/relay_info.go: GenRelayInfo handles RelayFormatVolc
  with nil request as task-style relay info (with TaskRelayInfo)
- relay/channel/task/doubao/adaptor.go:
  - ValidateRequestAndSetAction: branches on RelayFormatVolc to parse
    Volc-native body minimally (model + content[] action detection)
  - BuildRequestBody: branches on RelayFormatVolc to forward raw bytes
    byte-identical; model-mapping patches only the model field
  - EstimateBilling: branches on RelayFormatVolc to read video_url
    from raw body content[] instead of TaskSubmitReq.Metadata
- router/video-router.go: volcV3Router uses new controllers directly,
  no VolcRequestConvert() middleware; relay_mode and task_id are set
  inline via route-level closures
- middleware/volc_adapter.go: DELETED
- middleware/volc_adapter_test.go: DELETED

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a mechanism for shipping default tiered-billing expressions
in code. Default expressions activate only when:
  - Admin hasn't set BillingMode in DB
  - Admin hasn't set BillingExpr in DB
  - Admin hasn't customized ModelRatio in DB

Architecture: B (lazy fallback). GetBillingMode/GetBillingExpr check
defaultBillingExpr at lookup time with no startup mutation. Chosen over
Architecture A because the DB load (loadOptionsFromDatabase) runs *after*
InitRatioSettings, so at apply-time we cannot distinguish a DB-set ratio
from a code-default ratio that the admin resaved; lazy lookup avoids the
ambiguity entirely and keeps the hot path a simple two-map check.

Added ratio_setting.IsModelRatioCustomized(model) to detect whether an
admin explicitly changed ModelRatio for a model: if the model is absent
from defaultModelRatio, any value in modelRatioMap came from DB; if it
is present in defaultModelRatio, a differing value signals a DB override.

The default map is empty in this commit; Step 4 populates it with
seedance video expressions covering 1080p resolution and video-input
discount tiers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This reverts commit e6d31ca8c.

Defaults for Seedance billing will live in the UI's existing
PRESET_GROUPS template list in TieredPricingEditor.jsx instead of
a Go-side fallback mechanism. Existing deployments stay on legacy
ratio mode unchanged; admins who want tiered billing for these
models opt in via the UI preset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds six tiered_expr presets to the Tiered Pricing Editor's
"请求条件" group covering doubao-seedance models with their actual
RMB/1M-output-token rates:

  - Seedance 2.0:        46/28 base, 51/31 at 1080p (text/with-video)
  - Seedance 2.0 Fast:   37/22 (text/with-video)
  - Seedance 1.5 Pro:    16/8 (with-audio/silent)
  - Seedance 1.0 Pro:    15/7.5 (online/flex)
  - Seedance 1.0 Pro Fast: 4.2/2.1 (online/flex)
  - Seedance 1.0 Lite:   10/5 (online/flex)

Each expression coalesces native Volc body shape (top-level fields)
and OpenAI-format-wrapped shape (metadata.*) so the same preset
applies whether the request hits /api/v3/* or /v1/video/generations
on a VolcAdapter channel.

Token estimation formula per Volc docs:
  (input_video_duration + output_video_duration) ×
  output_width × output_height × output_fps / 1024
Final billing uses upstream's usage.completion_tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous preset format `(cond) ? tier(...) : tier(...)` failed to
produce a valid cost estimate in the UI cost-estimator (evalExprLocally).
The local JS evaluator builds a minimal env {p, c, len, tier, math-helpers}
and calls `new Function(...)` on the raw expression string. When param()
appears before tier() — i.e. as the condition of the outermost ternary —
JavaScript throws `ReferenceError: param is not defined` before tier() is
ever reached, so the estimator shows an error and reports cost=0.

Investigation findings:
- BOTH old and new formats compile and run correctly in the Go billingexpr
  engine (pkg/billingexpr); smokeTestExpr passes for both forms.
- The failure is UI-only: evalExprLocally in TieredPricingEditor.jsx did
  not include param(), header(), has(), or nil in its JS eval environment.
- With the multiplicative form `tier("base", c * X) * (param(...) ? ...)`,
  tier() executes first (JavaScript evaluates left-to-right) and returns a
  valid float. param() is still evaluated as part of the multiplier, so the
  error still occurs — meaning the fix also requires adding param/nil stubs
  to evalExprLocally. Both changes are included in this commit.
- Root cause: evalExprLocally is a preview-only estimator; it intentionally
  cannot inspect the real request body. param() stubs returning null allow
  the expression to evaluate to the base-tier price (all multipliers resolve
  to their else-branch with null inputs), giving a meaningful preview.

Changes:
1. Rewrites all 6 Seedance presets as `tier("base", c * <base>) * <mult>`
   - doubao-seedance-2-0:          46 base × (res+video multipliers)
   - doubao-seedance-2-0-fast:     37 base × (video multiplier)
   - doubao-seedance-1-5-pro:      16 base × (silent 0.5 multiplier)
   - doubao-seedance-1-0-pro:      15 base × (flex 0.5 multiplier)
   - doubao-seedance-1-0-pro-fast: 4.2 base × (flex 0.5 multiplier)
   - doubao-seedance-1-0-lite:     10 base × (flex 0.5 multiplier)

2. Adds param(), header(), has(), nil stubs to evalExprLocally so any
   expression using request probes renders a valid base-price preview.

3. Adds permanent regression test at pkg/billingexpr/seedance_presets_test.go
   covering every tier × body-shape combination (31 cases total):
   - 8 cases for sd2-0 (4 combos × 2 body shapes)
   - 4 cases for sd2-0-fast (2 combos × 2 body shapes)
   - 5 cases for sd1-5-pro (3 native + 2 wrapped, including default)
   - 4 cases for sd1-0-pro, sd1-0-pro-fast, sd1-0-lite (2 × 2 each)
   - 6 smoke-test cases (one per model via billing_setting.SmokeTestExpr)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous inline param() form failed in upstream new-api v0.13.1's
evalExprLocally because that version lacks param/header stubs. Switching
to structured `requestRules` (matching existing claude-opus-fast /
gpt-5.4-tiers pattern) keeps `expr` as just `tier("base", c * BASE)` so
preview evaluates cleanly while the backend still receives the full
conditional expression after combineBillingExpr.

Multiplicative composition is used instead of a 4-way matrix because
the rule system doesn't support negation. For Seedance 2.0, this
introduces ~0.13% drift on the 1080p+video tier (31.04 instead of
31.00); accepted as Volc may publish 31 as a rounded display price.

Each conditional dimension has 2 rules (top-level path + metadata.*
path) to handle both /api/v3 native and /v1/video/generations OpenAI-
format request shapes.

Also relocates the regression test from pkg/billingexpr (upstream
shared) to relay/channel/volcadapter (Graylight channel that hosts
seedance models) for cleaner upstream merge boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Comment out EndpointTypeVolcImage/Video and the VolcAdapter case in
   GetEndpointTypesByChannelType, matching upstream's pattern of leaving
   task-style channels (Kling/Jimeng/Suno/Midjourney) without dedicated
   endpoint types. Channel 58 falls through to the OpenAI default in
   the marketplace UI, consistent with existing task channels.

2. Merge RelayTaskVolcSubmit / RelayTaskFetchVolc / RelayTaskVolcDelete
   back into the existing RelayTask / RelayTaskFetch by reading
   RelayFormat from a context key ("relay_format"). Removes ~90 lines of
   duplicated retry-loop logic. Routes set the format inline before
   dispatching; RelayTaskVolcDelete is replaced by a 5-line inline closure.

3. Translate VolcAdapter channel selector label to Chinese to match
   the rest of channel.constants.js ('火山方舟兼容 (Seedream + Seedance)').

4. Remove the param/header/has/nil stubs added to evalExprLocally in
   commit 81d37dced. They became dead code after switching to structured
   requestRules — the expr field is now just `tier("base", c * X)` with
   no param() calls.

5. Drop the redundant EndpointTypeImageGeneration entry from the
   VolcAdapter image-model return list (now moot since the case is
   commented out). Test cases updated to use string literals for
   volc-image/volc-video absence assertions since the constants are
   now commented out.

Plan Y (extending task billing with tiered_expr support) is still
being discussed; estimateBillingVolcNative, videoInputRatioMap, and
the seedance presets are intentionally untouched in this commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plan B1 implementation: extends task billing flow to support
BillingMode=tiered_expr, making the 6 seedance presets actually
fire (previously dead config — see commit message for fcfbb6629
for the discovery).

Investigation finding (Step 0): Option A — tiered_expr restricted to
RelayFormatVolc requests. EstimatedBillingTokens==0 guard ensures
OpenAI-format entry (/v1/video/generations) stays on ratio billing.
metadata.* rules dropped from all 6 seedance presets accordingly.

Pre-charge (relay/helper/price.go):
- ModelPriceHelperPerCall branches on BillingMode==tiered_expr &&
  EstimatedBillingTokens>0 → modelPriceHelperTieredForTask()
- Runs billingexpr.RunExprWithRequest() with estimated tokens,
  freezes BillingSnapshot + BillingRequestInput on RelayInfo

Token estimator (relay/channel/task/doubao/seedance_estimator.go):
- EstimateSeedanceTokens(modelName, body []byte) int64
- Formula: (inputVideoDurSec + outputDurSec) × W × H × 24 / 1024
- Conservative over-estimate: input video capped at 15 s
- Looks up model default resolution + max duration from tables
- Returns 0 on unknown model → falls back to ratio billing

Interface plumbing:
- TaskAdaptor.EstimateBillingTokens(c, info) added to interface
- BaseBilling provides zero default (no-op for non-seedance adaptors)
- Doubao TaskAdaptor overrides; guards on RelayFormatVolc
- relay_task.go sets info.EstimatedBillingTokens before price helper;
  wraps EstimateBilling OtherRatios in TieredBillingSnapshot==nil guard

Settlement (service/task_billing.go):
- RecalculateTaskQuotaByTokens branches on TieredSnapshot != nil
- Replays RunExprByHashWithRequest with actual token count
- Falls through to ratio settlement on expression error

Persistence (model/task.go, controller/relay.go):
- TaskBillingContext gains TieredSnapshot + TieredRequestBody fields
- controller/relay.go copies snapshot from RelayInfo to task record

Tests:
- seedance_estimator_test.go: 15 subtests covering resolution parsing,
  duration/frames, video-input detection, unknown model fallback
- seedance_presets_test.go: 22 tests updated for simplified expressions
  (metadata.* cases removed; Option A)
- go build ./relay/... ./service/... ./model/... ./controller/... clean
- go test ./relay/channel/task/doubao/... PASS (all 15)
- go test ./relay/channel/volcadapter/... PASS (all 22)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plan B1 (commit 975494f6f) implemented task tiered_expr support but
made three architectural mistakes:

1. Volc-specific code lived in taskdoubao.TaskAdaptor, polluting an
   adaptor shared by channels 45 (VolcEngine) / 54 (DoubaoVideo) /
   58 (VolcAdapter).

2. Tiered_expr settlement was added to generic service/task_billing.go
   RecalculateTaskQuotaByTokens, when the proper extension point —
   adaptor.AdjustBillingOnComplete — already exists in
   task_polling.go:540-560 (called BEFORE the token-recalc fallback).

3. BillingContext stored ~1-2 KB of raw request body per task to feed
   billingexpr param() lookups at settlement, while task.Data already
   has the Volc fetch response with resolution/duration/service_tier.

This commit:

- Extracts relay/channel/task/volcadapter/ as the dedicated adaptor
  for ChannelTypeVolcAdapter, embedding taskdoubao.TaskAdaptor for
  shared task plumbing (ParseTaskResult, FetchTask, etc).
- Cleans Volc-specific branches and helpers out of taskdoubao,
  bringing it close to its upstream/main state.
- Implements tiered_expr settle in volcadapter.AdjustBillingOnComplete,
  reading expression hash + 3 flags from BillingContext and
  resolution/duration/service_tier from task.Data (Volc fetch result).
- Reverts the tiered_expr branch in service/task_billing.go.
- Replaces TieredRequestBody []byte with TieredVolcFlags struct
  (~30 bytes vs ~1-2 KB per task).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e merge

1. Drop the now-commented EndpointTypeVolcImage/Video references from
   common/endpoint_defaults.go, common/endpoint_type.go, and
   constant/endpoint_type.go. These three files now match upstream/main
   byte-for-byte, simplifying future merges.

2. Inline volcRelayHandler (a one-line wrapper calling
   relay.VolcImageHelper) into the switch case in controller/relay.go,
   matching the style of WssHelper / ClaudeHelper / etc.

3. Remove ConvertVolcRequest from the Adaptor interface. It only had
   one real implementation (volcengine.Adaptor's no-op pass-through);
   all other 30+ stub implementations existed solely to satisfy the
   interface. Replace with a local type-assertion in
   relay/volc_handler.go's VolcImageHelper.

4. Merge relay/channel/volcadapter/ into relay/channel/task/volcadapter/.
   The former held only the channel's ModelList/ChannelName constants;
   the latter held the task adaptor. Same package name, same domain —
   no reason to split.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The DraftTask struct and ContentItem.DraftTask field were added in
cursor's feat/volc-adapter as part of the OpenAI-format draft_task
content type passthrough for Seedance 1.5 pro. Upstream new-api does
not have this feature.

VolcAdapter (channel 58) already supports draft_task via raw body
pass-through at /api/v3/*; users who want this feature should use
the native Volc-compat path. The OpenAI-format /v1/video/generations
entry on channels 45/54 reverts to upstream behavior (no DraftTask).

Reduces fork divergence from upstream/main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tions route

Path2RelayMode only handles /v1/... paths; the native Volc Ark image route
at /api/v3/images/generations was leaving RelayMode=0 (Unknown), causing
volcengine.Adaptor.GetRequestURL to return "unsupported relay mode: 0".

Fix: explicitly set relay_mode context key to RelayModeImagesGenerations
before calling controller.Relay, matching the pattern already used for
task routes in video-router.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs found and fixed during Volc SDK e2e testing:

1. relay/relay_task.go: videoFetchByIDRespBodyBuilder now returns the Volc-native
   ContentGenerationTask JSON (patching id to public task ID) when relay_format is
   "volc". Without this the SDK's polling loop received an internal TaskDto wrapper
   with code/data fields instead of the flat id/status/usage/content structure the
   SDK expects.

2. middleware/distributor.go: GET /api/v3/contents/generations/tasks/:id no longer
   requires a model name in the request body. The new branch sets
   shouldSelectChannel=false for GET (task fetch) and reads model from body for
   POST (task submit). Without this every task status poll aborted with "Model name
   not specified".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds unit tests for security boundaries identified during Tier 1
E2E review:
  - List filter user_id spoofing rejected (token-derived only)
  - Volc body validator handles malformed inputs cleanly
  - Task model not in any channel returns clean 4xx
  - Path traversal in task_id treated as literal lookup
  - Channel-Id header restricted to admin (verify upstream behavior preserved)

Tier 1 tests live in scripts/volc-local-test/ as E2E integration.
These are the unit-test counterparts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r channel

Reverses an earlier cleanup decision (commits f97a0c616 + 1f71ca830) that
commented out the volc-image and volc-video endpoint types to match
upstream's Kling/Jimeng/Suno/Midjourney pattern. UX feedback: with the
types removed, the model marketplace shows "openai" as the primary
endpoint for seedream/seedance models served by ChannelTypeVolcAdapter,
which is misleading — the gateway's native paths are /api/v3/*, not
/v1/*.

Restores:
  - EndpointTypeVolcImage = "volc-image" → /api/v3/images/generations
  - EndpointTypeVolcVideo = "volc-video" → /api/v3/contents/generations/tasks
  - GetEndpointTypesByChannelType[VolcAdapter] dispatches per-model:
    seedream → [volc-image, image-generation, openai]
    seedance → [volc-video, openai-video]
  - Tests guard that VolcEngine (45) and DoubaoVideo (54) still
    fall through to defaults — only channel 58 surfaces volc-* types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the 501 stub at DELETE /api/v3/contents/generations/tasks/:id.
Verifies ownership, forwards DELETE to upstream Volc with channel API key,
updates local task to cancelled status, and refunds pre-charge quota.
Handler uses only common/constant/logger/model/service — no graylight deps.

Ported from 5d9b05a2c (graylight/track-e-tos-signer), callback handler excluded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the 12 seedream-*/seedance-* alias entries (without the doubao-
prefix) and their two comment headers. ModelList now contains only the
12 canonical doubao-prefixed IDs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…S list

fix(billing): use gjson-standard double-quoted path in video_url check

The `param("content.#(type=='video_url')")` runtime check never fired because
gjson only accepts double-quoted string literals inside `#()` filters; the
single-quote form was treated as a literal apostrophe and never matched actual
values. This caused the 0.608696× video-input discount on
doubao-seedance-2-0-260128 and the 0.594595× discount on
doubao-seedance-2-0-fast-260128 to be silently skipped, overcharging admins
~64% for video-input requests.

Changes:
- TieredPricingEditor.jsx: update both seedance 2.0 PRESET_GROUPS entries to
  use `content.#(type=="video_url")` (double-quoted gjson syntax, JS
  single-quoted outer string).
- requestRuleExpr.js: replace all `[^"]+` path-capture regexes in
  tryParseRequestCondition with `((?:[^"\\]|\\.)*)` plus unescapePath() helper,
  so that round-tripping paths containing escaped double-quotes works correctly.
- requestRuleExpr.test.js: 7 vitest tests covering build→expr, expr→parse, full
  round-trip, multi-group (seedance 2.0), and a regression test proving the old
  single-quoted form was broken.
- web/package.json: add `test` script (`vitest run`).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ChannelTypeVolcAdapter (58) previously routed through APITypeVolcEngine
to volcengine.Adaptor, which required adding a no-op ConvertVolcRequest
to the volcengine package. This couples our new channel to the existing
Volc channel and means any change to volcengine implicitly affects
volcadapter.

Add a dedicated APITypeVolcAdapter and channel.Adaptor implementation
under relay/channel/volcadapter/. The new adaptor only supports the
Volc-native /api/v3/images/generations endpoint; all other relay modes
return a clear "not supported" error directing users to the volcengine
channel for OpenAI-format requests.

relay/channel/volcengine/ is now identical to upstream/main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VolcTaskDelete and its helpers (buildVolcDeleteResp, isTerminalStatus,
volcDeleteMapStatus) previously lived in controller/graylight/, which
the upstream PR cannot depend on. Move them to controller/ proper —
the package was also empty after this move, so the directory is gone.

router/video-router.go now imports only the single controller package
instead of both controller and controller/graylight.

No behavior change.
…ugh paths

Both the image handler (relay/volc_handler.go) and the task adaptor
(relay/channel/task/volcadapter/adaptor.go) were forwarding raw body bytes
without applying model mapping or param override after reading from body storage.

Image path: extract applyVolcImagePatches helper that operates on
map[string]json.RawMessage so all unknown Volc-specific fields
(sequential_image_generation, optimize_prompt_options, watermark, etc.)
are preserved while patching "model" and injecting ParamOverride fields.

Task path: add ApplyParamOverrideWithRelayInfo call after the existing
patchVolcBodyModel / model-extraction block, before any future
injectSafetyIdentifier / injectCallbackURL steps.

Tests: 4 new unit tests for applyVolcImagePatches covering model mapping,
no-op when not mapped, param override injection, and both combined; 1 new
test for BuildRequestBody asserting ParamOverride is applied while preserving
unknown fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without an explicit p term in the expression, the frontend tier
breakdown UI fails to render the prompt-cost line. Adding p*0 keeps
the cost identical but lets the UI parse the expression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…billing.go

The seed contained ¥-denominated rates which only make sense for CN
deployments. Pre-seeding on every fresh install is wrong because
USD-denominated deployments would silently get ~7× over-charges. Admin
must configure billing per-deployment via the UI; the PRESET_GROUPS in
TieredPricingEditor.jsx still expose curated templates as a starting
point that admin can load and adjust before save.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
JAYotta and others added 5 commits May 8, 2026 15:41
Both edits crept in during the feat/volc-adapter merge but are pure
churn — the import reordering in video-router.go and the
make()→map[]{} swap + extra comment in tiered_billing.go don't
contribute to the seedance preset feature this branch carries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tryParseRequestCondition runs on every UI edit in the tiered-pricing
editor. Compiling six RegExp objects per call is avoidable overhead;
hoisting them to module-scope constants compiles them once at module
load. Also factors out QUOTED_VALUE_RE for the two has()/== templates
that share that fragment, and the numeric op→MATCH_* map.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The quoted-value regex fragment accepts any \\. escape sequence, but
JSON.parse only handles JSON's escape grammar (\", \\, \/, \b,
\f, \n, \r, \t, \uXXXX). When admins type a half-finished
expression like has(header("x"), "\q") the regex matches but
JSON.parse throws, bubbling up and crashing the editor. Guard both
JSON.parse call sites with safeJsonParse → null on failure, mirroring
the unescapePath try/catch pattern, and add tests for both cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Squashed cherry of the PR QuantumNous#5 review feedback train (private fork).
Generic-only files; graylight-specific callback wiring is left out.

Key changes:
- service/task_billing.go: settle now wraps task.Quota persistence,
  funding adjustment, used_quota / channel.used_quota updates and
  billing log in a single DB transaction. RowsAffected guard on the
  targeted UPDATE so a phantom task ID aborts the settle without
  applying side effects. Subscription updated_at now reuses the
  settle timestamp. Cache invalidation moved out-of-tx to keep the
  primary commit fast.
- service/task_polling.go: SettleTaskBillingOnComplete is exported,
  accepts a nil adaptor (token fallback path), prefers TotalTokens
  but falls back to CompletionTokens when only the latter is reported.
- model/task.go: TieredVolcFlags grows resolution / duration /
  service_tier fields so the tiered_expr settle path can evaluate
  param() lookups even on callback-only deployments where task.Data
  is just {"id":"..."}.
- controller/relay.go: extractVolcFlags refactored to call the new
  parseVolcDuration helper. parseVolcDuration accepts float64, int,
  int64, json.Number and string forms; rejects negative, non-integer
  and out-of-int-range values. The decoder uses json.Decoder.UseNumber
  so numeric JSON values arrive as json.Number rather than float64.
- relay/channel/task/volcadapter/adaptor.go: buildSynthesizedBody
  reads resolution / duration / service_tier from TieredVolcFlags
  first, falling back to task.Data only when flags are absent — fixes
  settle on callback-only deployments. AdjustBillingOnComplete comment
  updated to reflect exported SettleTaskBillingOnComplete name.

Tests:
- service/task_billing_test.go: regression tests for the targeted
  UPDATE persistence, RowsAffected==0 guard, funding rollback, CAS
  win/lose on both refund and settle, nil-adaptor token fallback,
  PerCallBilling skip with nil adaptor, effectiveTokenCount fallback.
- controller/relay_volc_flags_test.go: parseVolcDuration string /
  json.Number / oversized / non-integer cases.
- relay/channel/task/volcadapter/adaptor_test.go: two new cases for
  callback-path flags carrying resolution and flags taking priority
  over task.Data in buildSynthesizedBody.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 8, 2026 14:06
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d766c1e-30ff-4710-a241-f16aea8f8257

📥 Commits

Reviewing files that changed from the base of the PR and between 560ba57 and e39b809.

📒 Files selected for processing (56)
  • common/api_type.go
  • common/endpoint_defaults.go
  • common/endpoint_type.go
  • common/endpoint_type_test.go
  • common/model.go
  • constant/api_type.go
  • constant/channel.go
  • constant/channel_test.go
  • constant/endpoint_type.go
  • controller/channel-test.go
  • controller/model.go
  • controller/relay.go
  • controller/relay_volc_flags_test.go
  • controller/volc_task_delete.go
  • controller/volc_task_delete_test.go
  • dto/volc_image_request.go
  • middleware/distributor.go
  • middleware/distributor_test.go
  • model/task.go
  • relay/channel/adapter.go
  • relay/channel/task/doubao/adaptor_test.go
  • relay/channel/task/taskcommon/helpers.go
  • relay/channel/task/volcadapter/adaptor.go
  • relay/channel/task/volcadapter/adaptor_test.go
  • relay/channel/task/volcadapter/constants.go
  • relay/channel/task/volcadapter/constants_test.go
  • relay/channel/task/volcadapter/estimator_test.go
  • relay/channel/task/volcadapter/seedance_estimator.go
  • relay/channel/task/volcadapter/seedance_presets_test.go
  • relay/channel/task/volcadapter/volc_helpers.go
  • relay/channel/volcadapter/adaptor.go
  • relay/common/relay_info.go
  • relay/constant/relay_mode.go
  • relay/helper/price.go
  • relay/helper/valid_request.go
  • relay/helper/valid_request_volc_test.go
  • relay/relay_adaptor.go
  • relay/relay_adaptor_test.go
  • relay/relay_task.go
  • relay/relay_task_volc_list_test.go
  • relay/volc_handler.go
  • relay/volc_handler_test.go
  • relay/volc_task_test.go
  • router/relay-router.go
  • router/video-router.go
  • service/task_billing.go
  • service/task_billing_test.go
  • service/task_polling.go
  • setting/billing_setting/tiered_billing.go
  • types/relay_format.go
  • web/classic/package.json
  • web/classic/src/constants/channel.constants.js
  • web/classic/src/helpers/render.jsx
  • web/classic/src/pages/Setting/Ratio/components/TieredPricingEditor.jsx
  • web/classic/src/pages/Setting/Ratio/components/requestRuleExpr.js
  • web/classic/src/pages/Setting/Ratio/components/requestRuleExpr.test.js

Walkthrough

Adds a new VolcAdapter channel (58) with Ark-native image and video task routes, adaptors, distributor/routing, token estimation, tiered billing, task delete/fetch/list, model catalogs, request validation, and extensive tests. UI gains channel option and Seedance tier presets.

Changes

VolcAdapter Ark-native integration

Layer / File(s) Summary
Constants & Contracts
constant/*, common/*
New channel/API/endpoint constants and mappings; "seedream" classification; endpoint defaults and ordering; tests validate registry.
Controllers & Routing
controller/*, middleware/*
Relay switch for Volc, task delete handler, flags extraction, distributor path handling.
Request DTO & Validation
dto/*, relay/helper/*
VolcImageRequest with Extra field capture; request validators; distributor path recognition.
Adaptors
relay/channel/.../volcadapter/*, relay/relay_adaptor.go
Volc image adaptor (pass-through), Volc task adaptor with raw-body forwarding, token estimation, tiered settlement; adaptor registration.
Helpers & Pricing
relay/common/*, relay/helper/*
RelayInfo extended with EstimatedBillingTokens; tiered task pricing path; Volc request validation.
Task flows
relay/relay_task.go
Submission uses token estimation; Volc-native fetch-by-id/list builders and mode wiring.
Routing
router/*
New Volc image and task route registration with relay metadata and auth/distribution middleware.
Billing & Polling
service/*
Transactional settlement helpers; exported completion settlement with fallbacks; extensive tests.
UI & Presets
web/classic/*
Channel option/icon, Seedance tier presets, robust rule parser with tests; test script added.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Gateway
  participant Adaptor
  participant VolcArk
  participant Billing
  Client->>Gateway: POST /api/v3/images/generations (Volc)
  Gateway->>Adaptor: Convert/DoRequest (raw body)
  Adaptor->>VolcArk: Forward request
  VolcArk-->>Adaptor: Response
  Adaptor-->>Gateway: OpenAI-like result
  Gateway->>Billing: Post consume quota
  Billing-->>Gateway: OK
  Gateway-->>Client: 200 response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • Calcium-Ion
  • xyfacai

Poem

🐇 I hop through Ark's bright stream,
Seedream paints, Seedance gleams;
Bytes pass true, no fields astray,
Tiers compute what tokens say;
Tasks list, fetch, cancel—clean—
New blue channel joins the scene.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new Volc Ark native pass-through channel (ChannelTypeVolcAdapter / API type 58) that forwards Volc Ark /api/v3/* requests byte-identically (preserving Volc-private fields), plus introduces tiered_expr billing integration and a transactional async-task settlement refactor. It also updates the Classic web UI to provide Seedance tiered pricing presets and improves request-rule expression parsing.

Changes:

  • Add VolcAdapter (58) routing, adaptors, handlers, and Volc-native /api/v3/images/generations + /api/v3/contents/generations/tasks* endpoints.
  • Integrate tiered_expr for Volc task pricing (token estimation + settlement snapshotting) and refactor async task settle/refund to commit accounting atomically.
  • Enhance Classic UI tiered pricing editor presets and request-rule expression build/parse (with new tests).

Reviewed changes

Copilot reviewed 56 out of 56 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
web/classic/src/pages/Setting/Ratio/components/TieredPricingEditor.jsx Adds Doubao Seedance tiered pricing presets.
web/classic/src/pages/Setting/Ratio/components/requestRuleExpr.js Improves request-rule expression parsing/perf and escaping behavior.
web/classic/src/pages/Setting/Ratio/components/requestRuleExpr.test.js Adds vitest coverage for expression round-trips/escaping.
web/classic/src/helpers/render.jsx Adds icon mapping for channel type 58.
web/classic/src/constants/channel.constants.js Adds UI option for VolcAdapter channel type 58.
web/classic/package.json Adds vitest run test script.
types/relay_format.go Introduces RelayFormatVolc constant.
setting/billing_setting/tiered_billing.go Minor import formatting change.
service/task_polling.go Exports settle helper, adds nil-adaptor + token fallback logic.
service/task_billing.go Adds transactional settle for async task quota/funding/stat updates.
service/task_billing_test.go Adds extensive tests for new transactional settle logic and edge cases.
router/video-router.go Adds Volc-native task routes under /api/v3/.../tasks*.
router/relay-router.go Adds Volc-native image route /api/v3/images/generations.
relay/volc_task_test.go Adds tests around Volc task routing/body passthrough (but contains stale DELETE stub expectation).
relay/volc_handler.go Implements Volc image handler with byte-level patching.
relay/volc_handler_test.go Adds tests for Volc image handler and patching behavior.
relay/relay_task.go Adds Volc-native task fetch/list response builders and tiered token estimation hook.
relay/relay_task_volc_list_test.go Adds tests for Volc task list builder filtering/security invariants.
relay/relay_adaptor.go Registers APITypeVolcAdapter adaptor + task adaptor routing for channel 58.
relay/relay_adaptor_test.go Adds tests validating adaptor routing for VolcAdapter.
relay/helper/valid_request.go Adds Volc image request validation dispatcher.
relay/helper/valid_request_volc_test.go Adds tests for Volc image validation parsing/robustness.
relay/helper/price.go Adds tiered_expr pricing branch for async tasks using estimated tokens.
relay/constant/relay_mode.go Adds RelayModeVideoFetchList constant.
relay/common/relay_info.go Adds RelayFormatVolc relay-info creation and EstimatedBillingTokens field.
relay/channel/volcadapter/adaptor.go New image adaptor for VolcAdapter channel type 58.
relay/channel/task/volcadapter/volc_helpers.go Volc-native task request parsing + body patch helpers.
relay/channel/task/volcadapter/seedance_presets_test.go Adds regression tests for Seedance tiered_expr presets.
relay/channel/task/volcadapter/seedance_estimator.go Implements Seedance token estimation formula.
relay/channel/task/volcadapter/estimator_test.go Adds tests for Seedance estimator corner cases.
relay/channel/task/volcadapter/constants.go Adds VolcAdapter model list constants (Seedream/Seedance).
relay/channel/task/volcadapter/constants_test.go Adds tests for model list sanity (duplicates/type classification).
relay/channel/task/volcadapter/adaptor.go New VolcAdapter task adaptor: byte passthrough + tiered_expr settlement.
relay/channel/task/volcadapter/adaptor_test.go Adds unit tests for task adaptor settlement/body behavior.
relay/channel/task/taskcommon/helpers.go Extends base billing helpers with EstimateBillingTokens default.
relay/channel/task/doubao/adaptor_test.go Adds regression guards for legacy doubao task adaptor paths.
relay/channel/adapter.go Extends TaskAdaptor interface with EstimateBillingTokens.
model/task.go Adds TieredSnapshot + TieredVolcFlags to task billing context.
middleware/distributor.go Adds Volc-native task path handling in distributor model extraction.
middleware/distributor_test.go Adds tests for distributor security boundaries and Volc path behavior.
dto/volc_image_request.go Adds Volc image request DTO with Extra-field capture.
controller/volc_task_delete.go Adds DELETE task cancellation handler with refund + upstream forwarding.
controller/volc_task_delete_test.go Adds unit tests for delete response/status mapping helpers.
controller/relay.go Wires RelayFormatVolc image helper + snapshots Volc flags for tiered_expr.
controller/relay_volc_flags_test.go Adds tests for Volc flags extraction and duration parsing.
controller/model.go Publishes VolcAdapter model list for model listing endpoints.
controller/channel-test.go Updates channel test routing for seedream models under VolcAdapter/VolcEngine.
constant/endpoint_type.go Adds volc-image and volc-video endpoint types.
constant/channel.go Adds ChannelTypeVolcAdapter (58) and base URL mapping.
constant/channel_test.go Adds tests for VolcAdapter registration and base URL.
constant/api_type.go Adds APITypeVolcAdapter constant.
common/model.go Marks “seedream” as an image-generation model prefix.
common/endpoint_type.go Adds endpoint type resolution for VolcAdapter (image vs video).
common/endpoint_type_test.go Adds tests for VolcAdapter endpoint type mapping.
common/endpoint_defaults.go Adds default endpoint infos for volc-image/volc-video.
common/api_type.go Adds ChannelTypeVolcAdapter → APITypeVolcAdapter mapping.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread middleware/distributor.go
Comment on lines +265 to +279
} else if strings.HasPrefix(c.Request.URL.Path, "/api/v3/contents/generations/tasks") {
// Volc-native task routes (/api/v3/contents/generations/tasks and .../tasks/:id).
// GET requests (task fetch and list) do not need channel selection.
// POST requests (task submit) extract model from the request body.
if c.Request.Method == http.MethodGet {
shouldSelectChannel = false
} else if c.Request.Method == http.MethodPost {
req, err := getModelFromRequest(c)
if err != nil {
return nil, false, err
}
if req != nil {
modelRequest.Model = req.Model
}
}
Comment thread relay/volc_task_test.go
Comment on lines +13 to +28
// TestVolcTaskDelete_Returns501 verifies that the DELETE route for volc tasks
// returns 501 Not Implemented with a recognizable message.
// The route uses an inline handler (no dedicated controller function).
func TestVolcTaskDelete_Returns501(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
router := gin.New()

// Mirror the inline handler registered in router/video-router.go.
router.DELETE("/api/v3/contents/generations/tasks/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"code": "not_implemented",
"message": "DELETE /api/v3/contents/generations/tasks/:id is not supported yet",
"status_code": http.StatusNotImplemented,
})
})
Comment on lines +317 to +321
// Unescape a JSON-string-escaped path (e.g. content.#(type==\"video_url\") → content.#(type=="video_url")).
// Only handles the two escape sequences that JSON.stringify emits for path strings: \" and \\.
function unescapePath(raw) {
return raw.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
}
Comment on lines +344 to +356
// GetAndValidateVolcImageRequest parses the native Volc Ark image request body.
// Only model presence is strictly validated; prompt/image validation is relaxed
// because Volc supports both t2i (prompt required) and i2i (image required) in
// the same endpoint and we want to pass all other fields through byte-identical.
func GetAndValidateVolcImageRequest(c *gin.Context) (*dto.VolcImageRequest, error) {
req := &dto.VolcImageRequest{}
if err := common.UnmarshalBodyReusable(c, req); err != nil {
return nil, err
}
// Accept model, model_name, or req_key as model identifier (Volc uses all three)
if req.Model == "" {
return nil, errors.New("model is required")
}
Comment thread relay/relay_task.go
Comment on lines +448 to +454
if json.Unmarshal(t.Data, &probe) == nil {
if _, hasStatus := probe["status"]; hasStatus {
// task.Data is an upstream Volc response — return it with the
// public task ID so the SDK's polling loop can match responses.
probe["id"] = json.RawMessage(`"` + t.TaskID + `"`)
if patched, err := json.Marshal(probe); err == nil {
return patched
Comment thread relay/relay_task.go
Comment on lines +488 to +491
b, err := common.Marshal(synth)
if err != nil {
return []byte(`{"id":"` + t.TaskID + `","status":"` + arkStatus + `"}`)
}
Comment thread relay/relay_task.go
Comment on lines +576 to +583
func countFilteredVolcVideoTasks(userID int, queryParams model.SyncTaskQueryParams, modelFilter string, taskIDsFilter map[string]bool) int64 {
totalBase := model.TaskCountAllUserTask(userID, queryParams)
if totalBase <= 0 {
return 0
}
// DB layer has no model/task_ids columns for direct filtering; fetch then filter in-memory.
allTasks := model.TaskGetAllUserTask(userID, 0, int(totalBase), queryParams)
var filteredCount int64
Comment on lines +84 to +88
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodDelete, deleteURL, nil)
if reqErr != nil {
logger.LogError(c, "VolcTaskDelete: build DELETE request failed: "+reqErr.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
b, err := common.Marshal(synth)
if err != nil {
return []byte(`{"id":"` + t.TaskID + `","status":"` + arkStatus + `"}`)
Comment thread relay/volc_handler.go
Comment on lines +148 to +166
// On model-patch failure the function logs and continues with the un-patched body
// (conservative: avoids introducing a new error path for a non-critical patch).
// On param-override failure the function returns an error.
func applyVolcImagePatches(rawBytes []byte, info *relaycommon.RelayInfo) ([]byte, *types.NewAPIError) {
// 1. Model mapping patch — byte-level, preserves all unknown fields.
if info.IsModelMapped && info.UpstreamModelName != "" {
var bodyMap map[string]json.RawMessage
if err := common.Unmarshal(rawBytes, &bodyMap); err == nil {
if newModel, err := common.Marshal(info.UpstreamModelName); err == nil {
bodyMap["model"] = newModel
if patched, err := common.Marshal(bodyMap); err == nil {
rawBytes = patched
}
// Marshal of bodyMap failed: log and continue with un-patched body.
}
// Marshal of model string failed: log and continue.
}
// Unmarshal failed: log and continue with un-patched body.
}
@JAYotta JAYotta closed this May 8, 2026
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.

2 participants