From 08f88d25e588e90012ec9f0594f6ea8f8a1a2c3a Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Sat, 25 Jul 2026 22:05:09 +0800 Subject: [PATCH] feat: support Tencent TokenHub API key via OpenAI-compatible protocol (#6232) * feat: support tencent tokenhub api key via openai-compatible protocol * fix: use tokenhub base url for tencent api key channels * test: cover tencent key-format dispatch and add TokenHub key prompt locales --- relay/channel/tencent/dispatch.go | 37 +++++++++++++ relay/channel/tencent/dispatch_test.go | 72 ++++++++++++++++++++++++++ relay/common/relay_info.go | 1 + relay/relay_adaptor.go | 2 +- web/src/features/channels/constants.ts | 2 +- web/src/i18n/locales/en.json | 2 +- web/src/i18n/locales/fr.json | 2 +- web/src/i18n/locales/ja.json | 2 +- web/src/i18n/locales/ru.json | 2 +- web/src/i18n/locales/vi.json | 2 +- web/src/i18n/locales/zh-TW.json | 2 +- web/src/i18n/locales/zh.json | 2 +- 12 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 relay/channel/tencent/dispatch.go create mode 100644 relay/channel/tencent/dispatch_test.go diff --git a/relay/channel/tencent/dispatch.go b/relay/channel/tencent/dispatch.go new file mode 100644 index 00000000..76ade5ca --- /dev/null +++ b/relay/channel/tencent/dispatch.go @@ -0,0 +1,37 @@ +package tencent + +import ( + "strings" + + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" +) + +const tokenHubBaseURL = "https://tokenhub.tencentmaas.com" + +// DispatchAdaptor 按密钥格式分流:三段式 ak/sk 走原生 TC3,单段 TokenHub key 走 OpenAI 兼容。 +type DispatchAdaptor struct { + channel.Adaptor +} + +func (a *DispatchAdaptor) Init(info *relaycommon.RelayInfo) { + if strings.Contains(info.ApiKey, "|") { + a.Adaptor = &Adaptor{} + } else { + a.Adaptor = &openai.Adaptor{} + if info.ChannelBaseUrl == "" || info.ChannelBaseUrl == constant.ChannelBaseURLs[constant.ChannelTypeTencent] { + info.ChannelBaseUrl = tokenHubBaseURL + } + } + a.Adaptor.Init(info) +} + +func (a *DispatchAdaptor) GetModelList() []string { + return ModelList +} + +func (a *DispatchAdaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/tencent/dispatch_test.go b/relay/channel/tencent/dispatch_test.go new file mode 100644 index 00000000..0c93d777 --- /dev/null +++ b/relay/channel/tencent/dispatch_test.go @@ -0,0 +1,72 @@ +package tencent + +import ( + "testing" + + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDispatchAdaptorInit(t *testing.T) { + tests := []struct { + name string + apiKey string + baseURL string + wantTC3 bool + wantBaseURL string + }{ + { + name: "legacy three-segment key selects TC3 adaptor and keeps base url", + apiKey: "1300000000|AKIDxxxxxxxx|secretxxxxxxxx", + baseURL: constant.ChannelBaseURLs[constant.ChannelTypeTencent], + wantTC3: true, + wantBaseURL: constant.ChannelBaseURLs[constant.ChannelTypeTencent], + }, + { + name: "tokenhub key with default base url rewrites to tokenhub", + apiKey: "sk-xxxxxxxxxxxxxxxx", + baseURL: constant.ChannelBaseURLs[constant.ChannelTypeTencent], + wantTC3: false, + wantBaseURL: tokenHubBaseURL, + }, + { + name: "tokenhub key with empty base url rewrites to tokenhub", + apiKey: "sk-xxxxxxxxxxxxxxxx", + baseURL: "", + wantTC3: false, + wantBaseURL: tokenHubBaseURL, + }, + { + name: "tokenhub key with custom base url is preserved", + apiKey: "sk-xxxxxxxxxxxxxxxx", + baseURL: "https://proxy.example.com", + wantTC3: false, + wantBaseURL: "https://proxy.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: constant.ChannelTypeTencent, + ApiKey: tt.apiKey, + ChannelBaseUrl: tt.baseURL, + }} + + dispatch := &DispatchAdaptor{} + dispatch.Init(info) + + require.NotNil(t, dispatch.Adaptor) + if tt.wantTC3 { + assert.IsType(t, &Adaptor{}, dispatch.Adaptor) + } else { + assert.IsType(t, &openai.Adaptor{}, dispatch.Adaptor) + } + assert.Equal(t, tt.wantBaseURL, info.ChannelBaseUrl) + }) + } +} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 9f460ce5..128e52e0 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -342,6 +342,7 @@ var streamSupportedChannels = map[int]bool{ constant.ChannelTypeMiniMax: true, constant.ChannelTypeSiliconFlow: true, constant.ChannelTypeAdvancedCustom: true, + constant.ChannelTypeTencent: true, } func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 29227446..8920ba8c 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -66,7 +66,7 @@ func GetAdaptor(apiType int) channel.Adaptor { case constant.APITypePaLM: return &palm.Adaptor{} case constant.APITypeTencent: - return &tencent.Adaptor{} + return &tencent.DispatchAdaptor{} case constant.APITypeXunfei: return &xunfei.Adaptor{} case constant.APITypeZhipu: diff --git a/web/src/features/channels/constants.ts b/web/src/features/channels/constants.ts index c218a5fe..556726bc 100644 --- a/web/src/features/channels/constants.ts +++ b/web/src/features/channels/constants.ts @@ -386,7 +386,7 @@ export const TYPE_TO_KEY_PROMPT: Record = { 15: 'Format: APIKey|SecretKey', 18: 'Format: APPID|APISecret|APIKey', 22: 'Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041', - 23: 'Format: AppId|SecretId|SecretKey', + 23: 'Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey', 33: 'Format: Ak|Sk|Region', 50: 'Format: AccessKey|SecretKey (or just ApiKey if upstream is New API)', 51: 'Format: Access Key ID|Secret Access Key', diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 609e72eb..f17db8f5 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2052,7 +2052,7 @@ "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041": "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", "Format: APIKey|SecretKey": "Format: APIKey|SecretKey", "Format: APPID|APISecret|APIKey": "Format: APPID|APISecret|APIKey", - "Format: AppId|SecretId|SecretKey": "Format: AppId|SecretId|SecretKey", + "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey": "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey", "Forward requests directly to upstream providers without any post-processing.": "Forward requests directly to upstream providers without any post-processing.", "Frames per second": "Frames per second", "Free": "Free", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 443b3fa1..aa342fc9 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2052,7 +2052,7 @@ "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041": "Format : APIKey-AppId, par ex. fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", "Format: APIKey|SecretKey": "Format : APIKey|SecretKey", "Format: APPID|APISecret|APIKey": "Format : APPID|APISecret|APIKey", - "Format: AppId|SecretId|SecretKey": "Format : AppId|SecretId|SecretKey", + "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey": "Format : TokenHub API Key, ou l'ancien format AppId|SecretId|SecretKey", "Forward requests directly to upstream providers without any post-processing.": "Transférer les requêtes directement aux fournisseurs amont sans aucun post-traitement.", "Frames per second": "Images par seconde", "Free": "Libre", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index c15de553..2abd64c1 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -2052,7 +2052,7 @@ "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041": "形式: APIKey-AppId、例: fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", "Format: APIKey|SecretKey": "形式: APIKey|SecretKey", "Format: APPID|APISecret|APIKey": "形式: APPID|APISecret|APIKey", - "Format: AppId|SecretId|SecretKey": "形式: AppId|SecretId|SecretKey", + "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey": "形式: TokenHub API Key、または旧形式の AppId|SecretId|SecretKey", "Forward requests directly to upstream providers without any post-processing.": "ポストプロセスなしで、リクエストをアップストリームプロバイダーに直接転送します。", "Frames per second": "フレームレート", "Free": "空き", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index a1c18a35..97568a29 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -2052,7 +2052,7 @@ "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041": "Формат: APIKey-AppId, напр., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", "Format: APIKey|SecretKey": "Формат: APIKey|SecretKey", "Format: APPID|APISecret|APIKey": "Формат: APPID|APISecret|APIKey", - "Format: AppId|SecretId|SecretKey": "Формат: AppId|SecretId|SecretKey", + "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey": "Формат: TokenHub API Key или устаревший AppId|SecretId|SecretKey", "Forward requests directly to upstream providers without any post-processing.": "Перенаправлять запросы напрямую upstream-провайдерам без какой-либо постобработки.", "Frames per second": "Кадров в секунду", "Free": "Свободно", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 19cdddb1..80957757 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -2052,7 +2052,7 @@ "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041": "Định dạng: APIKey-AppId, ví dụ: fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", "Format: APIKey|SecretKey": "Định dạng: APIKey|SecretKey", "Format: APPID|APISecret|APIKey": "Định dạng: APPID|APISecret|APIKey", - "Format: AppId|SecretId|SecretKey": "Định dạng: AppId|SecretId|SecretKey", + "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey": "Định dạng: TokenHub API Key, hoặc định dạng cũ AppId|SecretId|SecretKey", "Forward requests directly to upstream providers without any post-processing.": "Chuyển tiếp các yêu cầu trực tiếp đến các nhà cung cấp ngược dòng mà không cần xử lý hậu kỳ nào.", "Frames per second": "Khung hình / giây", "Free": "Trống", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index d895a2b3..c04a7356 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -2052,7 +2052,7 @@ "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041": "格式:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", "Format: APIKey|SecretKey": "格式:APIKey|SecretKey", "Format: APPID|APISecret|APIKey": "格式:APPID|APISecret|APIKey", - "Format: AppId|SecretId|SecretKey": "格式:AppId|SecretId|SecretKey", + "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey": "格式:TokenHub API Key,或舊版 AppId|SecretId|SecretKey", "Forward requests directly to upstream providers without any post-processing.": "將請求直接轉發給上游供應商,不進行任何後處理。", "Frames per second": "幀率", "Free": "可用", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index ee80b960..b19ec7c3 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -2052,7 +2052,7 @@ "Format: APIKey-AppId, e.g., fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041": "格式:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041", "Format: APIKey|SecretKey": "格式:APIKey|SecretKey", "Format: APPID|APISecret|APIKey": "格式:APPID|APISecret|APIKey", - "Format: AppId|SecretId|SecretKey": "格式:AppId|SecretId|SecretKey", + "Format: TokenHub API Key, or legacy AppId|SecretId|SecretKey": "格式:TokenHub API Key,或旧版 AppId|SecretId|SecretKey", "Forward requests directly to upstream providers without any post-processing.": "将请求直接转发给上游提供商,不进行任何后处理。", "Frames per second": "帧率", "Free": "可用",