From cc51aba7cc005e513836955f5b685ffc67ca7927 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 14 Apr 2026 16:57:02 +0800 Subject: [PATCH 1/4] 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 f4c322566703257409eb85b35e892563cf68196b Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 14 Apr 2026 18:19:02 +0800 Subject: [PATCH 2/4] 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. --- 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 { From 11fe2c762df398419b4ef51cc5cfd5a609c5ab3e Mon Sep 17 00:00:00 2001 From: liangchaoboy Date: Thu, 16 Apr 2026 13:16:29 +0800 Subject: [PATCH 3/4] docs: add docstrings to improve coverage --- common/api_type.go | 2 +- constant/channel.go | 2 +- controller/channel_upstream_update_qiniu_test.go | 2 ++ relay/channel/openai/adaptor.go | 1 + relay/common/relay_info.go | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/common/api_type.go b/common/api_type.go index 1bd85e146e2c..4b450b35c82b 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -8,7 +8,7 @@ func ChannelType2APIType(channelType int) (int, bool) { case constant.ChannelTypeOpenAI: apiType = constant.APITypeOpenAI case constant.ChannelTypeQiniu: - apiType = constant.APITypeOpenAI + apiType = constant.APITypeOpenAI // 七牛使用 OpenAI 兼容协议 case constant.ChannelTypeAnthropic: apiType = constant.APITypeAnthropic case constant.ChannelTypeBaidu: diff --git a/constant/channel.go b/constant/channel.go index 0715fcbdee00..42f9856a7c9c 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -55,7 +55,7 @@ const ( ChannelTypeSora = 55 ChannelTypeReplicate = 56 ChannelTypeCodex = 57 - ChannelTypeQiniu = 58 + ChannelTypeQiniu = 58 // 七牛 AI(OpenAI 兼容协议),默认 Base URL: https://api.qnaigc.com ChannelTypeDummy // this one is only for count, do not add any channel after this ) diff --git a/controller/channel_upstream_update_qiniu_test.go b/controller/channel_upstream_update_qiniu_test.go index d70b23600fcb..7addd3f4c895 100644 --- a/controller/channel_upstream_update_qiniu_test.go +++ b/controller/channel_upstream_update_qiniu_test.go @@ -10,6 +10,8 @@ import ( "github.com/QuantumNous/new-api/model" ) +// TestFetchChannelUpstreamModelIDs_Qiniu 验证七牛渠道通过 /v1/models 接口拉取上游模型列表的逻辑, +// 使用 httptest 模拟七牛 API 响应,确保请求路径、认证头和返回结果解析均正确。 func TestFetchChannelUpstreamModelIDs_Qiniu(t *testing.T) { t.Parallel() diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 6b71aa9448dc..53cd377b07b9 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -244,6 +244,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn if request == nil { return nil, errors.New("request is nil") } + // 仅对 streamSupportedChannels 中注册的渠道保留 StreamOptions,其余渠道置空以避免上游报错 if !info.SupportStreamOptions { request.StreamOptions = nil } diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 5c6ff1f81afb..7e76168bd5e6 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -305,7 +305,7 @@ func (info *RelayInfo) ToString() string { // 定义支持流式选项的通道类型 var streamSupportedChannels = map[int]bool{ constant.ChannelTypeOpenAI: true, - constant.ChannelTypeQiniu: true, + constant.ChannelTypeQiniu: true, // 七牛支持 stream_options 参数 constant.ChannelTypeAnthropic: true, constant.ChannelTypeAws: true, constant.ChannelTypeGemini: true, From f5daa130f2b9619d9b8f09d6ab531ae74f3fb92f Mon Sep 17 00:00:00 2001 From: liangchaoboy Date: Thu, 16 Apr 2026 13:31:12 +0800 Subject: [PATCH 4/4] docs: add docstrings to improve coverage --- constant/channel.go | 4 ++-- web/src/components/table/channels/modals/EditChannelModal.jsx | 2 +- web/src/constants/channel.constants.js | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/constant/channel.go b/constant/channel.go index 42f9856a7c9c..3ac1e2b05b42 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -119,7 +119,7 @@ var ChannelBaseURLs = []string{ "https://api.openai.com", //55 "https://api.replicate.com", //56 "https://chatgpt.com", //57 - "https://api.qnaigc.com", //58 + "https://api.qnaigc.com", //58 - Qiniu AI gateway } var ChannelTypeNames = map[int]string{ @@ -177,7 +177,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeSora: "Sora", ChannelTypeReplicate: "Replicate", ChannelTypeCodex: "Codex", - ChannelTypeQiniu: "Qiniu", + ChannelTypeQiniu: "Qiniu", // 七牛 AI 网关 } func GetChannelTypeName(channelType int) string { diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index c8843b80e6b5..f81902d79336 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -132,7 +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, + 58, // Qiniu ]); function type2secretPrompt(type) { diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 5e3cfb40375e..ad714c188690 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -189,6 +189,7 @@ export const CHANNEL_OPTIONS = [ color: 'blue', label: 'Codex (OpenAI OAuth)', }, + // Qiniu AI gateway — uses OpenAI-compatible protocol { value: 58, color: 'green', @@ -199,7 +200,7 @@ export const CHANNEL_OPTIONS = [ // 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, + 58, // Qiniu ]); export const MODEL_TABLE_PAGE_SIZE = 10;