From cfbc2df3f8b5f08dea7ba56c5ed5f8a4d8a54b70 Mon Sep 17 00:00:00 2001
From: DD <1083962986@qq.com>
Date: Mon, 8 Sep 2025 16:21:21 +0800
Subject: [PATCH 01/13] add submodel.ai
---
common/api_type.go | 2 +
constant/api_type.go | 1 +
constant/channel.go | 2 +
relay/channel/submodel/adaptor.go | 82 ++++++++++++++++++++++++++
relay/channel/submodel/constants.go | 16 +++++
relay/relay_adaptor.go | 3 +
setting/ratio_setting/model_ratio.go | 13 ++++
web/src/constants/channel.constants.js | 5 ++
web/src/helpers/render.js | 2 +
web/src/pages/Channel/EditTagModal.js | 3 +
10 files changed, 129 insertions(+)
create mode 100644 relay/channel/submodel/adaptor.go
create mode 100644 relay/channel/submodel/constants.go
diff --git a/common/api_type.go b/common/api_type.go
index f045866a..6204451d 100644
--- a/common/api_type.go
+++ b/common/api_type.go
@@ -65,6 +65,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeCoze
case constant.ChannelTypeJimeng:
apiType = constant.APITypeJimeng
+ case constant.ChannelTypeSubmodel:
+ apiType = constant.APITypeSubmodel
}
if apiType == -1 {
return constant.APITypeOpenAI, false
diff --git a/constant/api_type.go b/constant/api_type.go
index 6ba5f257..0c7b1fdd 100644
--- a/constant/api_type.go
+++ b/constant/api_type.go
@@ -31,5 +31,6 @@ const (
APITypeXai
APITypeCoze
APITypeJimeng
+ APITypeSubmodel
APITypeDummy // this one is only for count, do not add any channel after this
)
diff --git a/constant/channel.go b/constant/channel.go
index 224121e7..3d7158b1 100644
--- a/constant/channel.go
+++ b/constant/channel.go
@@ -49,6 +49,7 @@ const (
ChannelTypeCoze = 49
ChannelTypeKling = 50
ChannelTypeJimeng = 51
+ ChannelTypeSubmodel = 52
ChannelTypeDummy // this one is only for count, do not add any channel after this
)
@@ -106,4 +107,5 @@ var ChannelBaseURLs = []string{
"https://api.coze.cn", //49
"https://api.klingai.com", //50
"https://visual.volcengineapi.com", //51
+ "https://llm.submodel.ai", //52
}
diff --git a/relay/channel/submodel/adaptor.go b/relay/channel/submodel/adaptor.go
new file mode 100644
index 00000000..371fb055
--- /dev/null
+++ b/relay/channel/submodel/adaptor.go
@@ -0,0 +1,82 @@
+package submodel
+
+import (
+ "errors"
+ "io"
+ "net/http"
+ "one-api/dto"
+ "one-api/relay/channel"
+ "one-api/relay/channel/openai"
+ relaycommon "one-api/relay/common"
+ "one-api/types"
+
+ "github.com/gin-gonic/gin"
+)
+
+type Adaptor struct {
+}
+
+func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
+}
+
+func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
+ return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil
+}
+
+func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
+ channel.SetupApiRequestHeader(info, c, req)
+ req.Set("Authorization", "Bearer "+info.ApiKey)
+ return nil
+}
+
+func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
+ if request == nil {
+ return nil, errors.New("request is nil")
+ }
+ return request, nil
+}
+
+func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
+ return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
+ return channel.DoApiRequest(a, c, info, requestBody)
+}
+
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
+ if info.IsStream {
+ usage, err = openai.OaiStreamHandler(c, info, resp)
+ } else {
+ usage, err = openai.OpenaiHandler(c, info, resp)
+ }
+ return
+}
+
+func (a *Adaptor) GetModelList() []string {
+ return ModelList
+}
+
+func (a *Adaptor) GetChannelName() string {
+ return ChannelName
+}
\ No newline at end of file
diff --git a/relay/channel/submodel/constants.go b/relay/channel/submodel/constants.go
new file mode 100644
index 00000000..962682bb
--- /dev/null
+++ b/relay/channel/submodel/constants.go
@@ -0,0 +1,16 @@
+package submodel
+
+var ModelList = []string{
+ "NousResearch/Hermes-4-405B-FP8",
+ "Qwen/Qwen3-235B-A22B-Thinking-2507",
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8",
+ "Qwen/Qwen3-235B-A22B-Instruct-2507",
+ "zai-org/GLM-4.5-FP8",
+ "openai/gpt-oss-120b",
+ "deepseek-ai/DeepSeek-R1-0528",
+ "deepseek-ai/DeepSeek-R1",
+ "deepseek-ai/DeepSeek-V3-0324",
+ "deepseek-ai/DeepSeek-V3.1",
+}
+
+var ChannelName = "submodel"
\ No newline at end of file
diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go
index 2ce12a87..946053a2 100644
--- a/relay/relay_adaptor.go
+++ b/relay/relay_adaptor.go
@@ -34,6 +34,7 @@ import (
"one-api/relay/channel/xunfei"
"one-api/relay/channel/zhipu"
"one-api/relay/channel/zhipu_4v"
+ "one-api/relay/channel/submodel"
)
func GetAdaptor(apiType int) channel.Adaptor {
@@ -96,6 +97,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &coze.Adaptor{}
case constant.APITypeJimeng:
return &jimeng.Adaptor{}
+ case constant.APITypeSubmodel:
+ return &submodel.Adaptor{}
}
return nil
}
diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go
index 8a1d6aae..0bcb6ff5 100644
--- a/setting/ratio_setting/model_ratio.go
+++ b/setting/ratio_setting/model_ratio.go
@@ -223,6 +223,19 @@ var defaultModelRatio = map[string]float64{
"grok-vision-beta": 2.5,
"grok-3-fast-beta": 2.5,
"grok-3-mini-fast-beta": 0.3,
+
+ // submodel
+ "NousResearch/Hermes-4-405B-FP8": 0.8,
+ "Qwen/Qwen3-235B-A22B-Thinking-2507": 0.6,
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": 0.8,
+ "Qwen/Qwen3-235B-A22B-Instruct-2507": 0.3,
+ "zai-org/GLM-4.5-FP8": 0.8,
+ "openai/gpt-oss-120b": 0.5,
+ "deepseek-ai/DeepSeek-R1-0528": 0.8,
+ "deepseek-ai/DeepSeek-R1": 0.8,
+ "deepseek-ai/DeepSeek-V3-0324": 0.8,
+ "deepseek-ai/DeepSeek-V3.1": 0.8
+
}
var defaultModelPrice = map[string]float64{
diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js
index b145ea11..6a4566a7 100644
--- a/web/src/constants/channel.constants.js
+++ b/web/src/constants/channel.constants.js
@@ -135,6 +135,11 @@ export const CHANNEL_OPTIONS = [
color: 'blue',
label: '即梦',
},
+ {
+ value: 52,
+ color: 'blue',
+ label: 'SubModel',
+ },
];
export const MODEL_TABLE_PAGE_SIZE = 10;
diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js
index 34ba78d7..abf246b8 100644
--- a/web/src/helpers/render.js
+++ b/web/src/helpers/render.js
@@ -398,6 +398,8 @@ export function getChannelIcon(channelType) {
return ;
case 21: // 知识库:AI Proxy
case 44: // 嵌入模型:MokaAI M3E
+ case 52: // SubModel
+ return null;
default:
return null; // 未知类型或自定义渠道不显示图标
}
diff --git a/web/src/pages/Channel/EditTagModal.js b/web/src/pages/Channel/EditTagModal.js
index 433d4f09..35fc1646 100644
--- a/web/src/pages/Channel/EditTagModal.js
+++ b/web/src/pages/Channel/EditTagModal.js
@@ -98,6 +98,9 @@ const EditTagModal = (props) => {
case 36:
localModels = ['suno_music', 'suno_lyrics'];
break;
+ case 52:
+ localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1'];
+ break;
default:
localModels = getChannelModels(value);
break;
From 62a25874fa8ed03f64d1416b0519508d84fc2721 Mon Sep 17 00:00:00 2001
From: DD <1083962986@qq.com>
Date: Mon, 8 Sep 2025 17:33:15 +0800
Subject: [PATCH 02/13] merge
---
web/src/pages/Channel/EditTagModal.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/src/pages/Channel/EditTagModal.js b/web/src/pages/Channel/EditTagModal.js
index 35fc1646..aedf991c 100644
--- a/web/src/pages/Channel/EditTagModal.js
+++ b/web/src/pages/Channel/EditTagModal.js
@@ -99,7 +99,7 @@ const EditTagModal = (props) => {
localModels = ['suno_music', 'suno_lyrics'];
break;
case 52:
- localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1'];
+ localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8','Qwen/Qwen3-235B-A22B-Instruct-2507', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1'];
break;
default:
localModels = getChannelModels(value);
From 116d070bbf0c196d5b13d9333a54269cfc70fa09 Mon Sep 17 00:00:00 2001
From: DD <1083962986@qq.com>
Date: Wed, 10 Sep 2025 18:37:55 +0800
Subject: [PATCH 03/13] merge
---
constant/channel.go | 6 ++----
web/src/helpers/render.jsx | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/constant/channel.go b/constant/channel.go
index 0cfd90cd..34fb20f4 100644
--- a/constant/channel.go
+++ b/constant/channel.go
@@ -49,13 +49,11 @@ const (
ChannelTypeCoze = 49
ChannelTypeKling = 50
ChannelTypeJimeng = 51
-<<<<<<< HEAD
- ChannelTypeSubmodel = 52
-=======
ChannelTypeVidu = 52
->>>>>>> 041782c49e0289b9d2e64a318e81e4f75754dabf
+ ChannelTypeSubmodel = 53
ChannelTypeDummy // this one is only for count, do not add any channel after this
+
)
var ChannelBaseURLs = []string{
diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx
index 49e87f66..676a582b 100644
--- a/web/src/helpers/render.jsx
+++ b/web/src/helpers/render.jsx
@@ -342,7 +342,7 @@ export function getChannelIcon(channelType) {
return ;
case 21: // 知识库:AI Proxy
case 44: // 嵌入模型:MokaAI M3E
- case 52: // SubModel
+ case 53: // SubModel
return null;
default:
return null; // 未知类型或自定义渠道不显示图标
From 68d30225e5e5f9be628b3303db809da25a80cbb5 Mon Sep 17 00:00:00 2001
From: DD <1083962986@qq.com>
Date: Wed, 10 Sep 2025 19:11:58 +0800
Subject: [PATCH 04/13] merge
---
relay/channel/submodel/constants.go | 2 +-
setting/ratio_setting/model_ratio.go | 4 +---
web/src/components/table/channels/modals/EditTagModal.jsx | 2 +-
web/src/helpers/render.jsx | 2 --
4 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/relay/channel/submodel/constants.go b/relay/channel/submodel/constants.go
index 962682bb..f5e1feb8 100644
--- a/relay/channel/submodel/constants.go
+++ b/relay/channel/submodel/constants.go
@@ -13,4 +13,4 @@ var ModelList = []string{
"deepseek-ai/DeepSeek-V3.1",
}
-var ChannelName = "submodel"
\ No newline at end of file
+const ChannelName = "submodel"
\ No newline at end of file
diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go
index 26db8168..c427cfe2 100644
--- a/setting/ratio_setting/model_ratio.go
+++ b/setting/ratio_setting/model_ratio.go
@@ -251,7 +251,6 @@ var defaultModelRatio = map[string]float64{
"grok-vision-beta": 2.5,
"grok-3-fast-beta": 2.5,
"grok-3-mini-fast-beta": 0.3,
-
// submodel
"NousResearch/Hermes-4-405B-FP8": 0.8,
"Qwen/Qwen3-235B-A22B-Thinking-2507": 0.6,
@@ -262,8 +261,7 @@ var defaultModelRatio = map[string]float64{
"deepseek-ai/DeepSeek-R1-0528": 0.8,
"deepseek-ai/DeepSeek-R1": 0.8,
"deepseek-ai/DeepSeek-V3-0324": 0.8,
- "deepseek-ai/DeepSeek-V3.1": 0.8
-
+ "deepseek-ai/DeepSeek-V3.1": 0.8,
}
var defaultModelPrice = map[string]float64{
diff --git a/web/src/components/table/channels/modals/EditTagModal.jsx b/web/src/components/table/channels/modals/EditTagModal.jsx
index 727b1909..752ff3dc 100644
--- a/web/src/components/table/channels/modals/EditTagModal.jsx
+++ b/web/src/components/table/channels/modals/EditTagModal.jsx
@@ -118,7 +118,7 @@ const EditTagModal = (props) => {
case 36:
localModels = ['suno_music', 'suno_lyrics'];
break;
- case 52:
+ case 53:
localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8','Qwen/Qwen3-235B-A22B-Instruct-2507', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1'];
break;
default:
diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx
index 676a582b..3d9d8d71 100644
--- a/web/src/helpers/render.jsx
+++ b/web/src/helpers/render.jsx
@@ -342,8 +342,6 @@ export function getChannelIcon(channelType) {
return ;
case 21: // 知识库:AI Proxy
case 44: // 嵌入模型:MokaAI M3E
- case 53: // SubModel
- return null;
default:
return null; // 未知类型或自定义渠道不显示图标
}
From 1c48391cdb08c71efcc156a8a09e1394f1eb43ed Mon Sep 17 00:00:00 2001
From: DD <1083962986@qq.com>
Date: Mon, 15 Sep 2025 14:31:31 +0800
Subject: [PATCH 05/13] add submodel icon
---
web/src/helpers/render.jsx | 139 +++++++++++++++++++------------------
1 file changed, 71 insertions(+), 68 deletions(-)
diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx
index 65332701..c6926258 100644
--- a/web/src/helpers/render.jsx
+++ b/web/src/helpers/render.jsx
@@ -54,6 +54,7 @@ import {
FastGPT,
Kling,
Jimeng,
+ SubModel,
} from '@lobehub/icons';
import {
@@ -342,6 +343,8 @@ export function getChannelIcon(channelType) {
return ;
case 21: // 知识库:AI Proxy
case 44: // 嵌入模型:MokaAI M3E
+ case 53: // 嵌入模型:SubModel
+ return ;
default:
return null; // 未知类型或自定义渠道不显示图标
}
@@ -1191,25 +1194,25 @@ export function renderModelPrice(
const extraServices = [
webSearch && webSearchCallCount > 0
? i18next.t(
- ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
- {
- count: webSearchCallCount,
- price: webSearchPrice,
- ratio: groupRatio,
- ratioType: ratioLabel,
- },
- )
+ ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
+ {
+ count: webSearchCallCount,
+ price: webSearchPrice,
+ ratio: groupRatio,
+ ratioType: ratioLabel,
+ },
+ )
: '',
fileSearch && fileSearchCallCount > 0
? i18next.t(
- ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
- {
- count: fileSearchCallCount,
- price: fileSearchPrice,
- ratio: groupRatio,
- ratioType: ratioLabel,
- },
- )
+ ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
+ {
+ count: fileSearchCallCount,
+ price: fileSearchPrice,
+ ratio: groupRatio,
+ ratioType: ratioLabel,
+ },
+ )
: '',
].join('');
@@ -1379,10 +1382,10 @@ export function renderAudioModelPrice(
let audioPrice =
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
(audioCompletionTokens / 1000000) *
- inputRatioPrice *
- audioRatio *
- audioCompletionRatio *
- groupRatio;
+ inputRatioPrice *
+ audioRatio *
+ audioCompletionRatio *
+ groupRatio;
let price = textPrice + audioPrice;
return (
<>
@@ -1438,27 +1441,27 @@ export function renderAudioModelPrice(
{cacheTokens > 0
? i18next.t(
- '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
- {
- nonCacheInput: inputTokens - cacheTokens,
- cacheInput: cacheTokens,
- cachePrice: inputRatioPrice * cacheRatio,
- price: inputRatioPrice,
- completion: completionTokens,
- compPrice: completionRatioPrice,
- total: textPrice.toFixed(6),
- },
- )
+ '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+ {
+ nonCacheInput: inputTokens - cacheTokens,
+ cacheInput: cacheTokens,
+ cachePrice: inputRatioPrice * cacheRatio,
+ price: inputRatioPrice,
+ completion: completionTokens,
+ compPrice: completionRatioPrice,
+ total: textPrice.toFixed(6),
+ },
+ )
: i18next.t(
- '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
- {
- input: inputTokens,
- price: inputRatioPrice,
- completion: completionTokens,
- compPrice: completionRatioPrice,
- total: textPrice.toFixed(6),
- },
- )}
+ '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+ {
+ input: inputTokens,
+ price: inputRatioPrice,
+ completion: completionTokens,
+ compPrice: completionRatioPrice,
+ total: textPrice.toFixed(6),
+ },
+ )}
{i18next.t(
@@ -1598,35 +1601,35 @@ export function renderClaudeModelPrice(
{cacheTokens > 0 || cacheCreationTokens > 0
? i18next.t(
- '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
- {
- nonCacheInput: nonCachedTokens,
- cacheInput: cacheTokens,
- cacheRatio: cacheRatio,
- cacheCreationInput: cacheCreationTokens,
- cacheCreationRatio: cacheCreationRatio,
- cachePrice: cacheRatioPrice,
- cacheCreationPrice: cacheCreationRatioPrice,
- price: inputRatioPrice,
- completion: completionTokens,
- compPrice: completionRatioPrice,
- ratio: groupRatio,
- ratioType: ratioLabel,
- total: price.toFixed(6),
- },
- )
+ '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
+ {
+ nonCacheInput: nonCachedTokens,
+ cacheInput: cacheTokens,
+ cacheRatio: cacheRatio,
+ cacheCreationInput: cacheCreationTokens,
+ cacheCreationRatio: cacheCreationRatio,
+ cachePrice: cacheRatioPrice,
+ cacheCreationPrice: cacheCreationRatioPrice,
+ price: inputRatioPrice,
+ completion: completionTokens,
+ compPrice: completionRatioPrice,
+ ratio: groupRatio,
+ ratioType: ratioLabel,
+ total: price.toFixed(6),
+ },
+ )
: i18next.t(
- '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
- {
- input: inputTokens,
- price: inputRatioPrice,
- completion: completionTokens,
- compPrice: completionRatioPrice,
- ratio: groupRatio,
- ratioType: ratioLabel,
- total: price.toFixed(6),
- },
- )}
+ '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
+ {
+ input: inputTokens,
+ price: inputRatioPrice,
+ completion: completionTokens,
+ compPrice: completionRatioPrice,
+ ratio: groupRatio,
+ ratioType: ratioLabel,
+ total: price.toFixed(6),
+ },
+ )}
{i18next.t('仅供参考,以实际扣费为准')}
From 64e2ae56a2fb5cb098190c1fa0c3375491cf5569 Mon Sep 17 00:00:00 2001
From: dd <1083962986@qq.com>
Date: Mon, 29 Sep 2025 15:11:17 +0800
Subject: [PATCH 06/13] Update relay_adaptor.go
---
relay/relay_adaptor.go | 4 ----
1 file changed, 4 deletions(-)
diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go
index 6099385c..406074c5 100644
--- a/relay/relay_adaptor.go
+++ b/relay/relay_adaptor.go
@@ -37,12 +37,8 @@ import (
"one-api/relay/channel/zhipu"
"one-api/relay/channel/zhipu_4v"
"strconv"
-<<<<<<< HEAD
"one-api/relay/channel/submodel"
-=======
-
"github.com/gin-gonic/gin"
->>>>>>> 4f760a8d407d321bf7f011331ecffb2744b555fd
)
func GetAdaptor(apiType int) channel.Adaptor {
From d46e6d42efda916646f7bab27fc6ceb2013b0a8e Mon Sep 17 00:00:00 2001
From: dd <1083962986@qq.com>
Date: Mon, 29 Sep 2025 15:14:02 +0800
Subject: [PATCH 07/13] Update render.jsx
---
web/src/helpers/render.jsx | 3 ---
1 file changed, 3 deletions(-)
diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx
index 92fecde4..82d164b3 100644
--- a/web/src/helpers/render.jsx
+++ b/web/src/helpers/render.jsx
@@ -54,7 +54,6 @@ import {
FastGPT,
Kling,
Jimeng,
- SubModel,
} from '@lobehub/icons';
import {
@@ -343,8 +342,6 @@ export function getChannelIcon(channelType) {
return ;
case 21: // 知识库:AI Proxy
case 44: // 嵌入模型:MokaAI M3E
- case 53: // 嵌入模型:SubModel
- return ;
default:
return null; // 未知类型或自定义渠道不显示图标
}
From 509b23982dcb162a87f8a1e99e0564b615dca455 Mon Sep 17 00:00:00 2001
From: dd <1083962986@qq.com>
Date: Mon, 29 Sep 2025 19:37:04 +0800
Subject: [PATCH 08/13] Update constant/api_type.go
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
constant/api_type.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/constant/api_type.go b/constant/api_type.go
index b3c0a5c7..0ea5048f 100644
--- a/constant/api_type.go
+++ b/constant/api_type.go
@@ -31,7 +31,7 @@ const (
APITypeXai
APITypeCoze
APITypeJimeng
- APITypeMoonshot // this one is only for count, do not add any channel after this
- APITypeDummy // this one is only for count, do not add any channel after this
- APITypeSubmodel
+ APITypeMoonshot
+ APITypeSubmodel
+ APITypeDummy // this one is only for count, do not add any channel after this
)
From 60045cdceba10aa798e97aaa27528d97926f33e4 Mon Sep 17 00:00:00 2001
From: Seefs
Date: Mon, 29 Sep 2025 21:55:34 +0800
Subject: [PATCH 09/13] fix: submodel adapter
---
relay/channel/submodel/adaptor.go | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/relay/channel/submodel/adaptor.go b/relay/channel/submodel/adaptor.go
index 152391d0..db58fe64 100644
--- a/relay/channel/submodel/adaptor.go
+++ b/relay/channel/submodel/adaptor.go
@@ -16,6 +16,10 @@ import (
type Adaptor struct {
}
+func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
+ return nil, errors.New("submodel channel: endpoint not supported")
+}
+
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
return nil, errors.New("submodel channel: endpoint not supported")
}
@@ -32,7 +36,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
- return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil
+ return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), nil
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
From c00e5ca201af52284ce469b0fe6812a7f9d5f834 Mon Sep 17 00:00:00 2001
From: RedwindA
Date: Mon, 29 Sep 2025 22:16:25 +0800
Subject: [PATCH 10/13] fix: Optimize sidebar refresh to avoid redundant
loading states
---
web/src/components/layout/SiderBar.jsx | 2 +-
web/src/hooks/common/useHeaderBar.js | 2 +-
web/src/hooks/common/useSidebar.js | 46 +++++++++++++++++++++-----
3 files changed, 39 insertions(+), 11 deletions(-)
diff --git a/web/src/components/layout/SiderBar.jsx b/web/src/components/layout/SiderBar.jsx
index 793e4835..39d6d448 100644
--- a/web/src/components/layout/SiderBar.jsx
+++ b/web/src/components/layout/SiderBar.jsx
@@ -58,7 +58,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
loading: sidebarLoading,
} = useSidebar();
- const showSkeleton = useMinimumLoadingTime(sidebarLoading);
+ const showSkeleton = useMinimumLoadingTime(sidebarLoading, 200);
const [selectedKeys, setSelectedKeys] = useState(['home']);
const [chatItems, setChatItems] = useState([]);
diff --git a/web/src/hooks/common/useHeaderBar.js b/web/src/hooks/common/useHeaderBar.js
index 3458a1d1..f3ec8696 100644
--- a/web/src/hooks/common/useHeaderBar.js
+++ b/web/src/hooks/common/useHeaderBar.js
@@ -40,7 +40,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const location = useLocation();
const loading = statusState?.status === undefined;
- const isLoading = useMinimumLoadingTime(loading);
+ const isLoading = useMinimumLoadingTime(loading, 200);
const systemName = getSystemName();
const logo = getLogo();
diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js
index 13d76fd8..0ccc5835 100644
--- a/web/src/hooks/common/useSidebar.js
+++ b/web/src/hooks/common/useSidebar.js
@@ -17,7 +17,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import { useState, useEffect, useMemo, useContext } from 'react';
+import { useState, useEffect, useMemo, useContext, useRef } from 'react';
import { StatusContext } from '../../context/Status';
import { API } from '../../helpers';
@@ -29,6 +29,13 @@ export const useSidebar = () => {
const [statusState] = useContext(StatusContext);
const [userConfig, setUserConfig] = useState(null);
const [loading, setLoading] = useState(true);
+ const instanceIdRef = useRef(null);
+ const hasLoadedOnceRef = useRef(false);
+
+ if (!instanceIdRef.current) {
+ const randomPart = Math.random().toString(16).slice(2);
+ instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
+ }
// 默认配置
const defaultAdminConfig = {
@@ -74,9 +81,17 @@ export const useSidebar = () => {
}, [statusState?.status?.SidebarModulesAdmin]);
// 加载用户配置的通用方法
- const loadUserConfig = async () => {
+ const loadUserConfig = async ({ withLoading } = {}) => {
+ const shouldShowLoader =
+ typeof withLoading === 'boolean'
+ ? withLoading
+ : !hasLoadedOnceRef.current;
+
try {
- setLoading(true);
+ if (shouldShowLoader) {
+ setLoading(true);
+ }
+
const res = await API.get('/api/user/self');
if (res.data.success && res.data.data.sidebar_modules) {
let config;
@@ -122,18 +137,25 @@ export const useSidebar = () => {
});
setUserConfig(defaultUserConfig);
} finally {
- setLoading(false);
+ if (shouldShowLoader) {
+ setLoading(false);
+ }
+ hasLoadedOnceRef.current = true;
}
};
// 刷新用户配置的方法(供外部调用)
const refreshUserConfig = async () => {
- if (Object.keys(adminConfig).length > 0) {
- await loadUserConfig();
+ if (Object.keys(adminConfig).length > 0) {
+ await loadUserConfig({ withLoading: false });
}
// 触发全局刷新事件,通知所有useSidebar实例更新
- sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
+ sidebarEventTarget.dispatchEvent(
+ new CustomEvent(SIDEBAR_REFRESH_EVENT, {
+ detail: { sourceId: instanceIdRef.current, skipLoader: true },
+ }),
+ );
};
// 加载用户配置
@@ -146,9 +168,15 @@ export const useSidebar = () => {
// 监听全局刷新事件
useEffect(() => {
- const handleRefresh = () => {
+ const handleRefresh = (event) => {
+ if (event?.detail?.sourceId === instanceIdRef.current) {
+ return;
+ }
+
if (Object.keys(adminConfig).length > 0) {
- loadUserConfig();
+ loadUserConfig({
+ withLoading: event?.detail?.skipLoader ? false : undefined,
+ });
}
};
From 112ab0b17ca75ee09968e4d6404b975cfff0461b Mon Sep 17 00:00:00 2001
From: papersnake
Date: Tue, 30 Sep 2025 09:14:12 +0800
Subject: [PATCH 11/13] feat: support claude-sonnet-4-5-20250929
---
relay/channel/aws/constants.go | 6 ++++++
relay/channel/claude/constants.go | 2 ++
relay/channel/vertex/adaptor.go | 1 +
setting/ratio_setting/cache_ratio.go | 4 ++++
setting/ratio_setting/model_ratio.go | 1 +
5 files changed, 14 insertions(+)
diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go
index 5ac7ce99..45112d23 100644
--- a/relay/channel/aws/constants.go
+++ b/relay/channel/aws/constants.go
@@ -16,6 +16,7 @@ var awsModelIDMap = map[string]string{
"claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0",
"claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0",
"claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0",
+ "claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5-20250929-v1:0",
// Nova models
"nova-micro-v1:0": "amazon.nova-micro-v1:0",
"nova-lite-v1:0": "amazon.nova-lite-v1:0",
@@ -69,6 +70,11 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{
"anthropic.claude-opus-4-1-20250805-v1:0": {
"us": true,
},
+ "anthropic.claude-sonnet-4-5-20250929-v1:0": {
+ "us": true,
+ "ap": true,
+ "eu": true,
+ },
// Nova models - all support three major regions
"amazon.nova-micro-v1:0": {
"us": true,
diff --git a/relay/channel/claude/constants.go b/relay/channel/claude/constants.go
index a23543d2..d0b36fe4 100644
--- a/relay/channel/claude/constants.go
+++ b/relay/channel/claude/constants.go
@@ -19,6 +19,8 @@ var ModelList = []string{
"claude-opus-4-20250514-thinking",
"claude-opus-4-1-20250805",
"claude-opus-4-1-20250805-thinking",
+ "claude-sonnet-4-5-20250929",
+ "claude-sonnet-4-5-20250929-thinking",
}
var ChannelName = "claude"
diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go
index a424cb1a..91a7f88c 100644
--- a/relay/channel/vertex/adaptor.go
+++ b/relay/channel/vertex/adaptor.go
@@ -37,6 +37,7 @@ var claudeModelMap = map[string]string{
"claude-sonnet-4-20250514": "claude-sonnet-4@20250514",
"claude-opus-4-20250514": "claude-opus-4@20250514",
"claude-opus-4-1-20250805": "claude-opus-4-1@20250805",
+ "claude-sonnet-4-5-20250929": "claude-sonnet-4-5@20250929",
}
const anthropicVersion = "vertex-2023-10-16"
diff --git a/setting/ratio_setting/cache_ratio.go b/setting/ratio_setting/cache_ratio.go
index 5993cdee..8e4b227a 100644
--- a/setting/ratio_setting/cache_ratio.go
+++ b/setting/ratio_setting/cache_ratio.go
@@ -52,6 +52,8 @@ var defaultCacheRatio = map[string]float64{
"claude-opus-4-20250514-thinking": 0.1,
"claude-opus-4-1-20250805": 0.1,
"claude-opus-4-1-20250805-thinking": 0.1,
+ "claude-sonnet-4-5-20250929": 0.1,
+ "claude-sonnet-4-5-20250929-thinking": 0.1,
}
var defaultCreateCacheRatio = map[string]float64{
@@ -69,6 +71,8 @@ var defaultCreateCacheRatio = map[string]float64{
"claude-opus-4-20250514-thinking": 1.25,
"claude-opus-4-1-20250805": 1.25,
"claude-opus-4-1-20250805-thinking": 1.25,
+ "claude-sonnet-4-5-20250929": 1.25,
+ "claude-sonnet-4-5-20250929-thinking": 1.25,
}
//var defaultCreateCacheRatio = map[string]float64{}
diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go
index 887c5bd5..2244593a 100644
--- a/setting/ratio_setting/model_ratio.go
+++ b/setting/ratio_setting/model_ratio.go
@@ -141,6 +141,7 @@ var defaultModelRatio = map[string]float64{
"claude-3-7-sonnet-20250219": 1.5,
"claude-3-7-sonnet-20250219-thinking": 1.5,
"claude-sonnet-4-20250514": 1.5,
+ "claude-sonnet-4-5-20250929": 1.5,
"claude-3-opus-20240229": 7.5, // $15 / 1M tokens
"claude-opus-4-20250514": 7.5,
"claude-opus-4-1-20250805": 7.5,
From a7f6065f425e9a94c727f7d8aa59c1e106cff522 Mon Sep 17 00:00:00 2001
From: Seefs
Date: Tue, 30 Sep 2025 09:22:40 +0800
Subject: [PATCH 12/13] feat: claude context editing
---
dto/claude.go | 9 +++++----
relay/channel/aws/adaptor.go | 4 ++++
relay/channel/claude/adaptor.go | 4 ++++
web/src/i18n/locales/en.json | 1 +
web/src/i18n/locales/fr.json | 1 +
web/src/pages/Setting/Model/SettingClaudeModel.jsx | 3 +--
6 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/dto/claude.go b/dto/claude.go
index 963e588b..42774226 100644
--- a/dto/claude.go
+++ b/dto/claude.go
@@ -196,10 +196,11 @@ type ClaudeRequest struct {
TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"`
//ClaudeMetadata `json:"metadata,omitempty"`
- Stream bool `json:"stream,omitempty"`
- Tools any `json:"tools,omitempty"`
- ToolChoice any `json:"tool_choice,omitempty"`
- Thinking *Thinking `json:"thinking,omitempty"`
+ Stream bool `json:"stream,omitempty"`
+ Tools any `json:"tools,omitempty"`
+ ContextManagement json.RawMessage `json:"context_management,omitempty"`
+ ToolChoice any `json:"tool_choice,omitempty"`
+ Thinking *Thinking `json:"thinking,omitempty"`
}
func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta {
diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go
index 9d5e5891..6202c9fc 100644
--- a/relay/channel/aws/adaptor.go
+++ b/relay/channel/aws/adaptor.go
@@ -52,6 +52,10 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
+ anthropicBeta := c.Request.Header.Get("anthropic-beta")
+ if anthropicBeta != "" {
+ req.Set("anthropic-beta", anthropicBeta)
+ }
model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req)
return nil
}
diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go
index 959327e1..fca26966 100644
--- a/relay/channel/claude/adaptor.go
+++ b/relay/channel/claude/adaptor.go
@@ -67,6 +67,10 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
anthropicVersion = "2023-06-01"
}
req.Set("anthropic-version", anthropicVersion)
+ anthropicBeta := c.Request.Header.Get("anthropic-beta")
+ if anthropicBeta != "" {
+ req.Set("anthropic-beta", anthropicBeta)
+ }
model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req)
return nil
}
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index 8a9d32f2..4e6c0ba8 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -1404,6 +1404,7 @@
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage",
"思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage",
"0.1-1之间的小数": "Decimal between 0.1 and 1",
+ "0.1以上的小数": "Decimal above 0.1",
"模型相关设置": "Model related settings",
"收起侧边栏": "Collapse sidebar",
"展开侧边栏": "Expand sidebar",
diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json
index 35a635bd..3a216e53 100644
--- a/web/src/i18n/locales/fr.json
+++ b/web/src/i18n/locales/fr.json
@@ -1404,6 +1404,7 @@
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Adaptation de la pensée Claude BudgetTokens = MaxTokens * BudgetTokens pourcentage",
"思考适配 BudgetTokens 百分比": "Adaptation de la pensée BudgetTokens pourcentage",
"0.1-1之间的小数": "Décimal entre 0,1 et 1",
+ "0.1以上的小数": "Décimal supérieur à 0,1",
"模型相关设置": "Paramètres liés au modèle",
"收起侧边栏": "Réduire la barre latérale",
"展开侧边栏": "Développer la barre latérale",
diff --git a/web/src/pages/Setting/Model/SettingClaudeModel.jsx b/web/src/pages/Setting/Model/SettingClaudeModel.jsx
index 04d7956a..688fc2d3 100644
--- a/web/src/pages/Setting/Model/SettingClaudeModel.jsx
+++ b/web/src/pages/Setting/Model/SettingClaudeModel.jsx
@@ -202,9 +202,8 @@ export default function SettingClaudeModel(props) {
label={t('思考适配 BudgetTokens 百分比')}
field={'claude.thinking_adapter_budget_tokens_percentage'}
initValue={''}
- extraText={t('0.1-1之间的小数')}
+ extraText={t('0.1以上的小数')}
min={0.1}
- max={1}
onChange={(value) =>
setInputs({
...inputs,
From 946da49721194018860e49fc51d24e01ba8e0604 Mon Sep 17 00:00:00 2001
From: Seefs
Date: Tue, 30 Sep 2025 09:46:46 +0800
Subject: [PATCH 13/13] fix: claude beta=true
---
relay/channel/claude/adaptor.go | 9 +++++++--
relay/common/relay_info.go | 6 +++++-
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go
index 959327e1..59f7dd0a 100644
--- a/relay/channel/claude/adaptor.go
+++ b/relay/channel/claude/adaptor.go
@@ -52,11 +52,16 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
+ baseURL := ""
if a.RequestMode == RequestModeMessage {
- return fmt.Sprintf("%s/v1/messages", info.ChannelBaseUrl), nil
+ baseURL = fmt.Sprintf("%s/v1/messages", info.ChannelBaseUrl)
} else {
- return fmt.Sprintf("%s/v1/complete", info.ChannelBaseUrl), nil
+ baseURL = fmt.Sprintf("%s/v1/complete", info.ChannelBaseUrl)
}
+ if info.IsClaudeBetaQuery {
+ baseURL = baseURL + "?beta=true"
+ }
+ return baseURL, nil
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go
index 99925dc5..f4ffaee2 100644
--- a/relay/common/relay_info.go
+++ b/relay/common/relay_info.go
@@ -105,7 +105,8 @@ type RelayInfo struct {
UserQuota int
RelayFormat types.RelayFormat
SendResponseCount int
- FinalPreConsumedQuota int // 最终预消耗的配额
+ FinalPreConsumedQuota int // 最终预消耗的配额
+ IsClaudeBetaQuery bool // /v1/messages?beta=true
PriceData types.PriceData
@@ -279,6 +280,9 @@ func GenRelayInfoClaude(c *gin.Context, request dto.Request) *RelayInfo {
info.ClaudeConvertInfo = &ClaudeConvertInfo{
LastMessagesType: LastMessageTypeNone,
}
+ if c.Query("beta") == "true" {
+ info.IsClaudeBetaQuery = true
+ }
return info
}