Просмотр исходного кода

Merge pull request #1920 from QuantumNous/alpha

Alpha -> main
Calcium-Ion 2 месяцев назад
Родитель
Сommit
595e3fed91

+ 2 - 0
common/api_type.go

@@ -67,6 +67,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
 		apiType = constant.APITypeJimeng
 	case constant.ChannelTypeMoonshot:
 		apiType = constant.APITypeMoonshot
+	case constant.ChannelTypeSubmodel:
+		apiType = constant.APITypeSubmodel
 	}
 	if apiType == -1 {
 		return constant.APITypeOpenAI, false

+ 3 - 2
constant/api_type.go

@@ -31,6 +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
+     APITypeMoonshot
+     APITypeSubmodel
+     APITypeDummy    // this one is only for count, do not add any channel after this
 )

+ 3 - 0
constant/channel.go

@@ -50,8 +50,10 @@ const (
 	ChannelTypeKling          = 50
 	ChannelTypeJimeng         = 51
 	ChannelTypeVidu           = 52
+	ChannelTypeSubmodel       = 53
 	ChannelTypeDummy          // this one is only for count, do not add any channel after this
 
+
 )
 
 var ChannelBaseURLs = []string{
@@ -108,4 +110,5 @@ var ChannelBaseURLs = []string{
 	"https://api.klingai.com",                   //50
 	"https://visual.volcengineapi.com",          //51
 	"https://api.vidu.cn",                       //52
+	"https://llm.submodel.ai",                   //53
 }

+ 5 - 4
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 {

+ 4 - 0
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
 }

+ 6 - 0
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,

+ 11 - 2
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 {
@@ -67,6 +72,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
 }

+ 2 - 0
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"

+ 86 - 0
relay/channel/submodel/adaptor.go

@@ -0,0 +1,86 @@
+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) 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")
+}
+
+func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
+}
+
+func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
+	return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, 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("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+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
+}

+ 16 - 0
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",
+}
+
+const ChannelName = "submodel"

+ 1 - 0
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"

+ 5 - 1
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
 }
 

+ 3 - 1
relay/relay_adaptor.go

@@ -37,7 +37,7 @@ import (
 	"one-api/relay/channel/zhipu"
 	"one-api/relay/channel/zhipu_4v"
 	"strconv"
-
+    "one-api/relay/channel/submodel"
 	"github.com/gin-gonic/gin"
 )
 
@@ -103,6 +103,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
 		return &jimeng.Adaptor{}
 	case constant.APITypeMoonshot:
 		return &moonshot.Adaptor{} // Moonshot uses Claude API
+	case constant.APITypeSubmodel:
+		return &submodel.Adaptor{}
 	}
 	return nil
 }

+ 4 - 0
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{}

+ 12 - 0
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,
@@ -251,6 +252,17 @@ 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{

+ 1 - 1
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([]);

+ 3 - 0
web/src/components/table/channels/modals/EditTagModal.jsx

@@ -118,6 +118,9 @@ const EditTagModal = (props) => {
         case 36:
           localModels = ['suno_music', 'suno_lyrics'];
           break;
+        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:
           localModels = getChannelModels(value);
           break;

+ 5 - 0
web/src/constants/channel.constants.js

@@ -159,6 +159,11 @@ export const CHANNEL_OPTIONS = [
     color: 'purple',
     label: 'Vidu',
   },
+   {
+    value: 53,
+    color: 'blue',
+    label: 'SubModel',
+  },
 ];
 
 export const MODEL_TABLE_PAGE_SIZE = 10;

+ 68 - 68
web/src/helpers/render.jsx

@@ -1200,25 +1200,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,
+                    },
+                  )
                   : '',
                 imageGenerationCall && imageGenerationCallPrice > 0
                   ? i18next.t(
@@ -1398,10 +1398,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 (
       <>
@@ -1457,27 +1457,27 @@ export function renderAudioModelPrice(
           <p>
             {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),
+                },
+              )}
           </p>
           <p>
             {i18next.t(
@@ -1617,35 +1617,35 @@ export function renderClaudeModelPrice(
           <p>
             {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),
+                },
+              )}
           </p>
           <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
         </article>

+ 1 - 1
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();

+ 37 - 9
web/src/hooks/common/useSidebar.js

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact [email protected]
 */
 
-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,
+        });
       }
     };
 

+ 1 - 0
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",

+ 1 - 0
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",

+ 1 - 2
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,