Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f55a2a0
feat: add volc compatibility routes
JAYotta Apr 27, 2026
643d900
feat: complete volc ark video task compatibility
JAYotta Apr 27, 2026
644c4f3
fix(volc-adapter): cleanup routing, constants, tests, and add volc en…
JAYotta Apr 27, 2026
0bb000c
refactor: serve volc-compat gateway at /api/v3 instead of /volc/api/v3
JAYotta Apr 27, 2026
053f9de
feat: add ChannelTypeVolcAdapter for volc-compat gateway
JAYotta Apr 27, 2026
165e695
revert: restore volcengine and doubao-video constants to upstream/main
JAYotta Apr 27, 2026
ebc47dc
refactor: native pass-through for /api/v3/images/generations
JAYotta Apr 27, 2026
f717d37
refactor: native pass-through for /api/v3/contents/generations/tasks*
JAYotta Apr 27, 2026
3a75408
feat(billing): add defaultBillingExpr infrastructure
JAYotta Apr 27, 2026
9534d4e
Revert "feat(billing): add defaultBillingExpr infrastructure"
JAYotta Apr 28, 2026
ef32fb9
feat(ui): add Seedance billing preset templates
JAYotta Apr 28, 2026
098646d
fix(ui): rewrite Seedance billing presets to use multiplicative form
JAYotta Apr 28, 2026
eb18d90
refactor(billing): use structured requestRules for Seedance presets
JAYotta Apr 28, 2026
791e358
fix(volc): batch review cleanups
JAYotta Apr 28, 2026
76eb73f
feat(billing): task tiered_expr support with seedance token estimation
JAYotta Apr 28, 2026
59e2009
refactor(volc): extract dedicated volcadapter task adaptor
JAYotta Apr 28, 2026
cd85ac4
refactor(volc): batch cleanup — endpoint types, dead wrappers, packag…
JAYotta Apr 28, 2026
1ed0ec4
refactor(doubao): drop cursor-added DraftTask struct
JAYotta Apr 28, 2026
552dfea
fix(router): set RelayModeImagesGenerations for /api/v3/images/genera…
JAYotta Apr 28, 2026
e270061
fix(relay): Volc-native task fetch returns ContentGenerationTask format
JAYotta Apr 28, 2026
ac26026
test(volc): boundary tests deferred from Tier 2/3 review
JAYotta Apr 28, 2026
351d907
feat(ui): restore volc-image/volc-video endpoint types for VolcAdapte…
JAYotta Apr 29, 2026
9a5834a
feat: add DELETE pass-through for Volc task cancellation
JAYotta Apr 29, 2026
20c355e
chore(volcadapter): drop bare-prefix model aliases from ModelList
JAYotta May 1, 2026
659a420
feat(billing): add 7 Doubao Seedance presets to existing PRESET_GROUP…
JAYotta May 1, 2026
84f3dc4
refactor(volcadapter): split image adaptor from volcengine
JAYotta May 8, 2026
b2b6690
refactor: move volc task delete handler out of graylight package
JAYotta May 8, 2026
3b5e47f
fix(volc): apply model mapping and param override on native pass-thro…
JAYotta May 8, 2026
10a0395
fix(billing): add p*0 placeholder to seedance lite expressions
JAYotta May 1, 2026
8feda1f
fix(billing): drop seedance defaultBillingMode/Expr seed from tiered_…
JAYotta May 1, 2026
6f8dbec
fix: revert cosmetic changes to video-router.go and tiered_billing.go
JAYotta May 1, 2026
8ea5f2f
perf(billing): hoist request-condition regex patterns to module scope
JAYotta May 1, 2026
f319f8a
fix(billing): safe-parse quoted values in has() request conditions
JAYotta May 1, 2026
3a689e3
fix(billing): correct Seedance 2.0 Fast token side
JAYotta May 2, 2026
e39b809
fix(billing): transactional task settle + parseVolcDuration evolution
JAYotta May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeReplicate
case constant.ChannelTypeCodex:
apiType = constant.APITypeCodex
case constant.ChannelTypeVolcAdapter:
apiType = constant.APITypeVolcAdapter
}
if apiType == -1 {
return constant.APITypeOpenAI, false
Expand Down
2 changes: 2 additions & 0 deletions common/endpoint_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
constant.EndpointTypeJinaRerank: {Path: "/v1/rerank", Method: "POST"},
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
constant.EndpointTypeVolcImage: {Path: "/api/v3/images/generations", Method: "POST"},
constant.EndpointTypeVolcVideo: {Path: "/api/v3/contents/generations/tasks", Method: "POST"},
}

// GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
Expand Down
12 changes: 12 additions & 0 deletions common/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
fallthrough
case constant.ChannelTypeGemini:
endpointTypes = []constant.EndpointType{constant.EndpointTypeGemini, constant.EndpointTypeOpenAI}
case constant.ChannelTypeVolcAdapter:
if IsImageGenerationModel(modelName) {
return []constant.EndpointType{
constant.EndpointTypeVolcImage,
constant.EndpointTypeImageGeneration,
constant.EndpointTypeOpenAI,
}
}
return []constant.EndpointType{
constant.EndpointTypeVolcVideo,
constant.EndpointTypeOpenAIVideo,
}
case constant.ChannelTypeOpenRouter: // OpenRouter 只支持 OpenAI 端点
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
case constant.ChannelTypeXai:
Expand Down
129 changes: 129 additions & 0 deletions common/endpoint_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package common

import (
"testing"

"github.com/QuantumNous/new-api/constant"
)

func TestGetEndpointTypesByChannelType(t *testing.T) {
type testCase struct {
name string
channelType int
modelName string
// wantFirst is the expected first element of the returned slice.
wantFirst constant.EndpointType
// wantContains lists types that must appear anywhere in the result.
wantContains []constant.EndpointType
// wantAbsent lists types that must NOT appear in the result.
wantAbsent []constant.EndpointType
// exactSlice, if non-nil, asserts the entire slice matches exactly.
exactSlice []constant.EndpointType
}

cases := []testCase{
// --- VolcAdapter (ch 58): image-gen models → [volc-image, image-generation, openai] ---
{
name: "VolcAdapter + seedream model → volc-image first",
channelType: constant.ChannelTypeVolcAdapter,
modelName: "doubao-seedream-5-0-260128",
wantFirst: constant.EndpointTypeVolcImage,
exactSlice: []constant.EndpointType{
constant.EndpointTypeVolcImage,
constant.EndpointTypeImageGeneration,
constant.EndpointTypeOpenAI,
},
},
// --- VolcAdapter (ch 58): video/non-image models → [volc-video, openai-video] ---
{
name: "VolcAdapter + seedance model → volc-video first",
channelType: constant.ChannelTypeVolcAdapter,
modelName: "doubao-seedance-2-0-260128",
wantFirst: constant.EndpointTypeVolcVideo,
exactSlice: []constant.EndpointType{
constant.EndpointTypeVolcVideo,
constant.EndpointTypeOpenAIVideo,
},
},
// --- Regression: VolcEngine (45) with seedream must NOT include volc-image ---
{
name: "VolcEngine (45) + seedream → no volc-image (only ch 58 gets volc-* types)",
channelType: constant.ChannelTypeVolcEngine,
modelName: "doubao-seedream-5-0-260128",
// VolcEngine falls to default; seedream triggers image-generation prepend only.
wantContains: []constant.EndpointType{constant.EndpointTypeImageGeneration},
wantAbsent: []constant.EndpointType{constant.EndpointTypeVolcImage},
},
// --- Regression: VolcEngine (45) + LLM → default openai ---
{
name: "VolcEngine (45) + LLM model → default openai",
channelType: constant.ChannelTypeVolcEngine,
modelName: "Doubao-pro-32k",
wantFirst: constant.EndpointTypeOpenAI,
wantAbsent: []constant.EndpointType{constant.EndpointTypeVolcImage, constant.EndpointTypeVolcVideo},
},
// --- Regression: DoubaoVideo (54) + seedance must NOT include volc-video ---
{
name: "DoubaoVideo (54) + seedance → no volc-video (only ch 58 gets volc-* types)",
channelType: constant.ChannelTypeDoubaoVideo,
modelName: "doubao-seedance-2-0-260128",
// DoubaoVideo falls to default; seedance is not an image model so openai is returned.
wantFirst: constant.EndpointTypeOpenAI,
wantAbsent: []constant.EndpointType{constant.EndpointTypeVolcVideo},
},
// --- DoubaoVideo (54) + arbitrary → default openai ---
{
name: "DoubaoVideo (54) + arbitrary model → default openai",
channelType: constant.ChannelTypeDoubaoVideo,
modelName: "some-video-model",
wantFirst: constant.EndpointTypeOpenAI,
wantAbsent: []constant.EndpointType{constant.EndpointTypeVolcVideo, constant.EndpointTypeVolcImage},
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got := GetEndpointTypesByChannelType(tc.channelType, tc.modelName)

if tc.exactSlice != nil {
if len(got) != len(tc.exactSlice) {
t.Fatalf("expected slice %v, got %v", tc.exactSlice, got)
}
for i, want := range tc.exactSlice {
if got[i] != want {
t.Errorf("index %d: want %q, got %q", i, want, got[i])
}
}
return
}

if tc.wantFirst != "" {
if len(got) == 0 || got[0] != tc.wantFirst {
t.Errorf("expected first element %q, got %v", tc.wantFirst, got)
}
}

contains := func(slice []constant.EndpointType, target constant.EndpointType) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}

for _, want := range tc.wantContains {
if !contains(got, want) {
t.Errorf("expected %q to be present in %v", want, got)
}
}

for _, absent := range tc.wantAbsent {
if contains(got, absent) {
t.Errorf("expected %q to be absent from %v", absent, got)
}
}
})
}
}
1 change: 1 addition & 0 deletions common/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
"prefix:imagen-",
"flux-",
"flux.1-",
"seedream",
}
OpenAITextModels = []string{
"gpt-",
Expand Down
1 change: 1 addition & 0 deletions constant/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ const (
APITypeMiniMax
APITypeReplicate
APITypeCodex
APITypeVolcAdapter
APITypeDummy // this one is only for count, do not add any channel after this
)
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
ChannelTypeSora = 55
ChannelTypeReplicate = 56
ChannelTypeCodex = 57
ChannelTypeVolcAdapter = 58
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -118,6 +119,7 @@ var ChannelBaseURLs = []string{
"https://api.openai.com", //55
"https://api.replicate.com", //56
"https://chatgpt.com", //57
"https://ark.cn-beijing.volces.com", //58
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeSora: "Sora",
ChannelTypeReplicate: "Replicate",
ChannelTypeCodex: "Codex",
ChannelTypeVolcAdapter: "VolcAdapter",
}

func GetChannelTypeName(channelType int) string {
Expand Down
54 changes: 54 additions & 0 deletions constant/channel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package constant

import (
"testing"
)

func TestChannelTypeVolcAdapterRegistration(t *testing.T) {
// Verify the constant value is 58 (next available after ChannelTypeCodex=57).
if ChannelTypeVolcAdapter != 58 {
t.Errorf("expected ChannelTypeVolcAdapter=58, got %d", ChannelTypeVolcAdapter)
}

// ChannelTypeDummy in Go const blocks without iota repeats the previous explicit
// value, so Dummy == VolcAdapter == 58. This is the intended design: Dummy is a
// sentinel for "number of channel types" so loops use <= ChannelTypeDummy.
if ChannelTypeDummy != ChannelTypeVolcAdapter {
t.Errorf("expected ChannelTypeDummy == ChannelTypeVolcAdapter (%d), got %d",
ChannelTypeVolcAdapter, ChannelTypeDummy)
}
}

func TestChannelTypeVolcAdapterDisplayName(t *testing.T) {
name := GetChannelTypeName(ChannelTypeVolcAdapter)
if name != "VolcAdapter" {
t.Errorf("expected display name %q, got %q", "VolcAdapter", name)
}
}

func TestChannelTypeVolcAdapterBaseURL(t *testing.T) {
const wantURL = "https://ark.cn-beijing.volces.com"
if ChannelTypeVolcAdapter >= len(ChannelBaseURLs) {
t.Fatalf("ChannelBaseURLs too short: len=%d, ChannelTypeVolcAdapter=%d", len(ChannelBaseURLs), ChannelTypeVolcAdapter)
}
got := ChannelBaseURLs[ChannelTypeVolcAdapter]
if got != wantURL {
t.Errorf("expected base URL %q, got %q", wantURL, got)
}
}

// TestChannelBaseURLsLength verifies the ChannelBaseURLs slice covers all channel
// types including ChannelTypeVolcAdapter, so no index-out-of-bounds can occur.
func TestChannelBaseURLsLength(t *testing.T) {
// ChannelBaseURLs must have at least ChannelTypeVolcAdapter+1 entries (indices 0..ChannelTypeVolcAdapter).
if len(ChannelBaseURLs) < ChannelTypeVolcAdapter+1 {
t.Errorf("ChannelBaseURLs has %d entries but needs at least %d to cover ChannelTypeVolcAdapter=%d",
len(ChannelBaseURLs), ChannelTypeVolcAdapter+1, ChannelTypeVolcAdapter)
}
}

func TestChannelTypeVolcAdapterInNames(t *testing.T) {
if _, ok := ChannelTypeNames[ChannelTypeVolcAdapter]; !ok {
t.Errorf("ChannelTypeVolcAdapter (%d) not found in ChannelTypeNames map", ChannelTypeVolcAdapter)
}
}
2 changes: 2 additions & 0 deletions constant/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const (
EndpointTypeImageGeneration EndpointType = "image-generation"
EndpointTypeEmbeddings EndpointType = "embeddings"
EndpointTypeOpenAIVideo EndpointType = "openai-video"
EndpointTypeVolcImage EndpointType = "volc-image"
EndpointTypeVolcVideo EndpointType = "volc-video"
//EndpointTypeMidjourney EndpointType = "midjourney-proxy"
//EndpointTypeSuno EndpointType = "suno-proxy"
//EndpointTypeKling EndpointType = "kling"
Expand Down
5 changes: 3 additions & 2 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
requestPath = "/v1/embeddings" // 修改请求路径
}

// VolcEngine 图像生成模型
if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
// VolcEngine / VolcAdapter 图像生成模型
if (channel.Type == constant.ChannelTypeVolcEngine || channel.Type == constant.ChannelTypeVolcAdapter) &&
strings.Contains(testModel, "seedream") {
requestPath = "/v1/images/generations"
}

Expand Down
12 changes: 12 additions & 0 deletions controller/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/lingyiwanwu"
"github.com/QuantumNous/new-api/relay/channel/minimax"
"github.com/QuantumNous/new-api/relay/channel/moonshot"
"github.com/QuantumNous/new-api/relay/channel/task/volcadapter"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
Expand Down Expand Up @@ -79,6 +80,14 @@ func init() {
OwnedBy: minimax.ChannelName,
})
}
for _, modelName := range volcadapter.ModelList {
openAIModels = append(openAIModels, dto.OpenAIModels{
Id: modelName,
Object: "model",
Created: 1626777600,
OwnedBy: volcadapter.ChannelName,
})
}
for modelName, _ := range constant.MidjourneyModel2Action {
openAIModels = append(openAIModels, dto.OpenAIModels{
Id: modelName,
Expand All @@ -104,6 +113,9 @@ func init() {
adaptor.Init(meta)
channelId2Models[i] = adaptor.GetModelList()
}
// VolcAdapter has its own curated model list (seedream + seedance) distinct from
// the underlying volcengine adaptor's LLM-focused list.
channelId2Models[constant.ChannelTypeVolcAdapter] = volcadapter.ModelList
openAIModels = lo.UniqBy(openAIModels, func(m dto.OpenAIModels) string {
return m.Id
})
Expand Down
Loading
Loading