From a7519dd30f94d6b33842b982798176b6376a7787 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 14 Apr 2026 16:57:02 +0800 Subject: [PATCH 1/2] feat: add Qiniu channel with model list fetch Introduce Qiniu (OpenAI-compatible) channel type, enable upstream /v1/models fetching in UI, and add a unit test for model listing. Made-with: Cursor --- common/api_type.go | 2 + constant/channel.go | 3 ++ .../channel_upstream_update_qiniu_test.go | 47 +++++++++++++++++++ relay/common/relay_info.go | 1 + .../channels/modals/EditChannelModal.jsx | 1 + web/src/constants/channel.constants.js | 6 +++ web/src/helpers/render.jsx | 1 + 7 files changed, 61 insertions(+) create mode 100644 controller/channel_upstream_update_qiniu_test.go diff --git a/common/api_type.go b/common/api_type.go index 39c1fe9a5406..1bd85e146e2c 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -7,6 +7,8 @@ func ChannelType2APIType(channelType int) (int, bool) { switch channelType { case constant.ChannelTypeOpenAI: apiType = constant.APITypeOpenAI + case constant.ChannelTypeQiniu: + apiType = constant.APITypeOpenAI case constant.ChannelTypeAnthropic: apiType = constant.APITypeAnthropic case constant.ChannelTypeBaidu: diff --git a/constant/channel.go b/constant/channel.go index 48502bedc52c..0715fcbdee00 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -55,6 +55,7 @@ const ( ChannelTypeSora = 55 ChannelTypeReplicate = 56 ChannelTypeCodex = 57 + ChannelTypeQiniu = 58 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -118,6 +119,7 @@ var ChannelBaseURLs = []string{ "https://api.openai.com", //55 "https://api.replicate.com", //56 "https://chatgpt.com", //57 + "https://api.qnaigc.com", //58 } var ChannelTypeNames = map[int]string{ @@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeSora: "Sora", ChannelTypeReplicate: "Replicate", ChannelTypeCodex: "Codex", + ChannelTypeQiniu: "Qiniu", } func GetChannelTypeName(channelType int) string { diff --git a/controller/channel_upstream_update_qiniu_test.go b/controller/channel_upstream_update_qiniu_test.go new file mode 100644 index 000000000000..d70b23600fcb --- /dev/null +++ b/controller/channel_upstream_update_qiniu_test.go @@ -0,0 +1,47 @@ +package controller + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/model" +) + +func TestFetchChannelUpstreamModelIDs_Qiniu(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("unexpected method: %s", r.Method) + } + if r.URL.Path != "/v1/models" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "Bearer test-key" { + t.Fatalf("unexpected Authorization header: %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"object":"list","data":[{"id":"deepseek/deepseek-v3.1-terminus-thinking"},{"id":"gpt-4"}]}`)) + })) + defer srv.Close() + + ch := &model.Channel{ + Id: 123, + Type: constant.ChannelTypeQiniu, + Key: "test-key", + Status: common.ChannelStatusEnabled, + BaseURL: common.GetPointer[string](srv.URL), + } + + got, err := fetchChannelUpstreamModelIDs(ch) + if err != nil { + t.Fatalf("fetchChannelUpstreamModelIDs returned error: %v", err) + } + if len(got) != 2 || got[0] != "deepseek/deepseek-v3.1-terminus-thinking" || got[1] != "gpt-4" { + t.Fatalf("unexpected models: %#v", got) + } +} + diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 0bb75ee61815..5c6ff1f81afb 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -305,6 +305,7 @@ func (info *RelayInfo) ToString() string { // 定义支持流式选项的通道类型 var streamSupportedChannels = map[int]bool{ constant.ChannelTypeOpenAI: true, + constant.ChannelTypeQiniu: true, constant.ChannelTypeAnthropic: true, constant.ChannelTypeAws: true, constant.ChannelTypeGemini: true, diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 899e290594b8..c8843b80e6b5 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -132,6 +132,7 @@ const DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL = 'doubao-coding-plan'; // 支持并且已适配通过接口获取模型列表的渠道类型 const MODEL_FETCHABLE_TYPES = new Set([ 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, + 58, ]); function type2secretPrompt(type) { diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 9fa78779de8f..5e3cfb40375e 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -189,11 +189,17 @@ export const CHANNEL_OPTIONS = [ color: 'blue', label: 'Codex (OpenAI OAuth)', }, + { + value: 58, + color: 'green', + label: 'Qiniu', + }, ]; // Channel types that support upstream model list fetching in UI. export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([ 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, + 58, ]); export const MODEL_TABLE_PAGE_SIZE = 10; diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index 0ad16bca130a..c7740369bfad 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -330,6 +330,7 @@ export function getChannelIcon(channelType) { case 1: // OpenAI case 3: // Azure OpenAI case 57: // Codex + case 58: // Qiniu (OpenAI compatible) return ; case 2: // Midjourney Proxy case 5: // Midjourney Proxy Plus From 7e8bd73e88f671e53ab757070d28413c063b1008 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 14 Apr 2026 18:19:02 +0800 Subject: [PATCH 2/2] fix: use SupportStreamOptions flag instead of hardcoded channel types Replace hardcoded OpenAI/Azure channel type check with the SupportStreamOptions flag so all channels registered in streamSupportedChannels (including Qiniu) correctly preserve StreamOptions in upstream requests. Co-Authored-By: Claude Opus 4.6 --- relay/channel/openai/adaptor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 56a58f286524..6b71aa9448dc 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -244,7 +244,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn if request == nil { return nil, errors.New("request is nil") } - if info.ChannelType != constant.ChannelTypeOpenAI && info.ChannelType != constant.ChannelTypeAzure { + if !info.SupportStreamOptions { request.StreamOptions = nil } if info.ChannelType == constant.ChannelTypeOpenRouter {