Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b9246f788 | |||
| f8a8dbec6e | |||
| 47a92439fc | |||
| 69e6d5ce51 | |||
| 32c09c26cb | |||
| f939c28b7a | |||
| a36d6de8d1 | |||
| 5bed29e2bd | |||
| 14959944fb | |||
| 314878ff20 | |||
| ec818773f5 | |||
| f9b17f1fa9 | |||
| b640118d74 | |||
| 7e4474e59b | |||
| 0787083310 | |||
| c1ed9d552e | |||
| aa6c894f56 |
@@ -65,6 +65,14 @@
|
||||
- Add suffix `-low` to set low reasoning effort
|
||||
17. 🔄 Thinking to content option `thinking_to_content` in `Channel->Edit->Channel Extra Settings`, default is `false`, when `true`, the `reasoning_content` of the thinking content will be converted to `<think>` tags and concatenated to the content returned.
|
||||
18. 🔄 Model rate limit, support setting total request limit and successful request limit in `System Settings->Rate Limit Settings`
|
||||
19. 💰 Cache billing support, when enabled can charge a configurable ratio for cache hits:
|
||||
1. Set `Prompt Cache Ratio` in `System Settings -> Operation Settings`
|
||||
2. Set `Prompt Cache Ratio` in channel settings, range 0-1 (e.g., 0.5 means 50% charge on cache hits)
|
||||
3. Supported channels:
|
||||
- [x] OpenAI
|
||||
- [x] Azure
|
||||
- [x] DeepSeek
|
||||
- [ ] Claude
|
||||
|
||||
## Model Support
|
||||
This version additionally supports:
|
||||
|
||||
@@ -74,6 +74,14 @@
|
||||
- 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
|
||||
18. 🔄 思考转内容,支持在 `渠道-编辑-渠道额外设置` 中设置 `thinking_to_content` 选项,默认`false`,开启后会将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回。
|
||||
19. 🔄 模型限流,支持在 `系统设置-速率限制设置` 中设置模型限流,支持设置总请求数限制和成功请求数限制
|
||||
20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
|
||||
1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项
|
||||
2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
|
||||
3. 支持的渠道:
|
||||
- [x] OpenAI
|
||||
- [x] Azure
|
||||
- [x] DeepSeek
|
||||
- [ ] Claude
|
||||
|
||||
## 模型支持
|
||||
此版本额外支持以下模型:
|
||||
|
||||
@@ -83,6 +83,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Set("channel", channel.Type)
|
||||
c.Set("base_url", channel.GetBaseURL())
|
||||
group, _ := model.GetUserGroup(1, false)
|
||||
c.Set("group", group)
|
||||
|
||||
middleware.SetupContextForSelectedChannel(c, channel, testModel)
|
||||
|
||||
@@ -158,7 +160,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
||||
tok := time.Now()
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
consumedTime := float64(milliseconds) / 1000.0
|
||||
other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio, priceData.ModelPrice)
|
||||
other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio,
|
||||
usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice)
|
||||
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, info.OriginModelName, "模型测试",
|
||||
quota, "模型测试", 0, quota, int(consumedTime), false, info.Group, other)
|
||||
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
)
|
||||
|
||||
func GetPricing(c *gin.Context) {
|
||||
@@ -39,7 +40,7 @@ func GetPricing(c *gin.Context) {
|
||||
}
|
||||
|
||||
func ResetModelRatio(c *gin.Context) {
|
||||
defaultStr := setting.DefaultModelRatio2JSONString()
|
||||
defaultStr := operation_setting.DefaultModelRatio2JSONString()
|
||||
err := model.UpdateOption("ModelRatio", defaultStr)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
@@ -48,7 +49,7 @@ func ResetModelRatio(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
err = setting.UpdateModelRatioByJSONString(defaultStr)
|
||||
err = operation_setting.UpdateModelRatioByJSONString(defaultStr)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
|
||||
@@ -99,6 +99,7 @@ type Message struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Prefix *bool `json:"prefix,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
||||
ToolCallId string `json:"tool_call_id,omitempty"`
|
||||
parsedContent []MediaContent
|
||||
|
||||
@@ -64,6 +64,7 @@ type ChatCompletionsStreamResponseChoice struct {
|
||||
type ChatCompletionsStreamResponseChoiceDelta struct {
|
||||
Content *string `json:"content,omitempty"`
|
||||
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
||||
Reasoning *string `json:"reasoning,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
ToolCalls []ToolCallResponse `json:"tool_calls,omitempty"`
|
||||
}
|
||||
@@ -80,14 +81,18 @@ func (c *ChatCompletionsStreamResponseChoiceDelta) GetContentString() string {
|
||||
}
|
||||
|
||||
func (c *ChatCompletionsStreamResponseChoiceDelta) GetReasoningContent() string {
|
||||
if c.ReasoningContent == nil {
|
||||
if c.ReasoningContent == nil && c.Reasoning == nil {
|
||||
return ""
|
||||
}
|
||||
return *c.ReasoningContent
|
||||
if c.ReasoningContent != nil {
|
||||
return *c.ReasoningContent
|
||||
}
|
||||
return *c.Reasoning
|
||||
}
|
||||
|
||||
func (c *ChatCompletionsStreamResponseChoiceDelta) SetReasoningContent(s string) {
|
||||
c.ReasoningContent = &s
|
||||
c.Reasoning = &s
|
||||
}
|
||||
|
||||
type ToolCallResponse struct {
|
||||
@@ -161,6 +166,7 @@ type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
PromptCacheHitTokens int `json:"prompt_cache_hit_tokens,omitempty"`
|
||||
PromptTokensDetails InputTokenDetails `json:"prompt_tokens_details"`
|
||||
CompletionTokenDetails OutputTokenDetails `json:"completion_tokens_details"`
|
||||
}
|
||||
|
||||
+9
-6
@@ -92,11 +92,12 @@ func InitOptionMap() {
|
||||
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
|
||||
common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
|
||||
common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
|
||||
common.OptionMap["ModelRatio"] = setting.ModelRatio2JSONString()
|
||||
common.OptionMap["ModelPrice"] = setting.ModelPrice2JSONString()
|
||||
common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString()
|
||||
common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
|
||||
common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
|
||||
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
|
||||
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
||||
common.OptionMap["CompletionRatio"] = setting.CompletionRatio2JSONString()
|
||||
common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString()
|
||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||
common.OptionMap["ChatLink"] = common.ChatLink
|
||||
common.OptionMap["ChatLink2"] = common.ChatLink2
|
||||
@@ -344,15 +345,17 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
case "DataExportDefaultTime":
|
||||
common.DataExportDefaultTime = value
|
||||
case "ModelRatio":
|
||||
err = setting.UpdateModelRatioByJSONString(value)
|
||||
err = operation_setting.UpdateModelRatioByJSONString(value)
|
||||
case "GroupRatio":
|
||||
err = setting.UpdateGroupRatioByJSONString(value)
|
||||
case "UserUsableGroups":
|
||||
err = setting.UpdateUserUsableGroupsByJSONString(value)
|
||||
case "CompletionRatio":
|
||||
err = setting.UpdateCompletionRatioByJSONString(value)
|
||||
err = operation_setting.UpdateCompletionRatioByJSONString(value)
|
||||
case "ModelPrice":
|
||||
err = setting.UpdateModelPriceByJSONString(value)
|
||||
err = operation_setting.UpdateModelPriceByJSONString(value)
|
||||
case "CacheRatio":
|
||||
err = operation_setting.UpdateCacheRatioByJSONString(value)
|
||||
case "TopUpLink":
|
||||
common.TopUpLink = value
|
||||
case "ChatLink":
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"one-api/common"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -65,14 +65,14 @@ func updatePricing() {
|
||||
ModelName: model,
|
||||
EnableGroup: groups,
|
||||
}
|
||||
modelPrice, findPrice := setting.GetModelPrice(model, false)
|
||||
modelPrice, findPrice := operation_setting.GetModelPrice(model, false)
|
||||
if findPrice {
|
||||
pricing.ModelPrice = modelPrice
|
||||
pricing.QuotaType = 1
|
||||
} else {
|
||||
modelRatio, _ := setting.GetModelRatio(model)
|
||||
modelRatio, _ := operation_setting.GetModelRatio(model)
|
||||
pricing.ModelRatio = modelRatio
|
||||
pricing.CompletionRatio = setting.GetCompletionRatio(model)
|
||||
pricing.CompletionRatio = operation_setting.GetCompletionRatio(model)
|
||||
pricing.QuotaType = 0
|
||||
}
|
||||
pricingMap = append(pricingMap, pricing)
|
||||
|
||||
@@ -4,10 +4,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
@@ -21,6 +17,11 @@ import (
|
||||
"one-api/service"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
|
||||
@@ -42,10 +43,15 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo
|
||||
}
|
||||
|
||||
hasThinkingContent := false
|
||||
hasContent := false
|
||||
var thinkingContent strings.Builder
|
||||
for _, choice := range lastStreamResponse.Choices {
|
||||
if len(choice.Delta.GetReasoningContent()) > 0 {
|
||||
hasThinkingContent = true
|
||||
break
|
||||
thinkingContent.WriteString(choice.Delta.GetReasoningContent())
|
||||
}
|
||||
if len(choice.Delta.GetContentString()) > 0 {
|
||||
hasContent = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +60,13 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo
|
||||
if hasThinkingContent {
|
||||
response := lastStreamResponse.Copy()
|
||||
for i := range response.Choices {
|
||||
response.Choices[i].Delta.SetContentString("<think>\n")
|
||||
response.Choices[i].Delta.SetReasoningContent("")
|
||||
// send `think` tag with thinking content
|
||||
response.Choices[i].Delta.SetContentString("<think>\n" + thinkingContent.String())
|
||||
response.Choices[i].Delta.ReasoningContent = nil
|
||||
response.Choices[i].Delta.Reasoning = nil
|
||||
}
|
||||
info.ThinkingContentInfo.IsFirstThinkingContent = false
|
||||
return helper.ObjectData(c, response)
|
||||
} else {
|
||||
return helper.ObjectData(c, lastStreamResponse)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +77,12 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo
|
||||
// Process each choice
|
||||
for i, choice := range lastStreamResponse.Choices {
|
||||
// Handle transition from thinking to content
|
||||
if len(choice.Delta.GetContentString()) > 0 && !info.ThinkingContentInfo.SendLastThinkingContent {
|
||||
if hasContent && !info.ThinkingContentInfo.SendLastThinkingContent {
|
||||
response := lastStreamResponse.Copy()
|
||||
for j := range response.Choices {
|
||||
response.Choices[j].Delta.SetContentString("\n</think>\n\n")
|
||||
response.Choices[j].Delta.SetReasoningContent("")
|
||||
response.Choices[j].Delta.SetContentString("\n</think>\n")
|
||||
response.Choices[j].Delta.ReasoningContent = nil
|
||||
response.Choices[j].Delta.Reasoning = nil
|
||||
}
|
||||
info.ThinkingContentInfo.SendLastThinkingContent = true
|
||||
helper.ObjectData(c, response)
|
||||
@@ -84,7 +91,12 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo
|
||||
// Convert reasoning content to regular content
|
||||
if len(choice.Delta.GetReasoningContent()) > 0 {
|
||||
lastStreamResponse.Choices[i].Delta.SetContentString(choice.Delta.GetReasoningContent())
|
||||
lastStreamResponse.Choices[i].Delta.SetReasoningContent("")
|
||||
lastStreamResponse.Choices[i].Delta.ReasoningContent = nil
|
||||
lastStreamResponse.Choices[i].Delta.Reasoning = nil
|
||||
} else if !hasThinkingContent && !hasContent {
|
||||
// flush thinking content
|
||||
lastStreamResponse.Choices[i].Delta.ReasoningContent = nil
|
||||
lastStreamResponse.Choices[i].Delta.Reasoning = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +190,10 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
//}
|
||||
for _, choice := range streamResponse.Choices {
|
||||
responseTextBuilder.WriteString(choice.Delta.GetContentString())
|
||||
|
||||
// handle both reasoning_content and reasoning
|
||||
responseTextBuilder.WriteString(choice.Delta.GetReasoningContent())
|
||||
|
||||
if choice.Delta.ToolCalls != nil {
|
||||
if len(choice.Delta.ToolCalls) > toolCount {
|
||||
toolCount = len(choice.Delta.ToolCalls)
|
||||
@@ -199,7 +214,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
//}
|
||||
for _, choice := range streamResponse.Choices {
|
||||
responseTextBuilder.WriteString(choice.Delta.GetContentString())
|
||||
responseTextBuilder.WriteString(choice.Delta.GetReasoningContent())
|
||||
responseTextBuilder.WriteString(choice.Delta.GetReasoningContent()) // This will handle both reasoning_content and reasoning
|
||||
if choice.Delta.ToolCalls != nil {
|
||||
if len(choice.Delta.ToolCalls) > toolCount {
|
||||
toolCount = len(choice.Delta.ToolCalls)
|
||||
@@ -239,6 +254,12 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
if !containStreamUsage {
|
||||
usage, _ = service.ResponseText2Usage(responseTextBuilder.String(), info.UpstreamModelName, info.PromptTokens)
|
||||
usage.CompletionTokens += toolCount * 7
|
||||
} else {
|
||||
if info.ChannelType == common.ChannelTypeDeepSeek {
|
||||
if usage.PromptCacheHitTokens != 0 {
|
||||
usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if info.ShouldIncludeUsage && !containStreamUsage {
|
||||
@@ -291,7 +312,7 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model
|
||||
if simpleResponse.Usage.TotalTokens == 0 || (simpleResponse.Usage.PromptTokens == 0 && simpleResponse.Usage.CompletionTokens == 0) {
|
||||
completionTokens := 0
|
||||
for _, choice := range simpleResponse.Choices {
|
||||
ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent, model)
|
||||
ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent+choice.Message.Reasoning, model)
|
||||
completionTokens += ctkm
|
||||
}
|
||||
simpleResponse.Usage = dto.Usage{
|
||||
|
||||
@@ -6,30 +6,33 @@ import (
|
||||
"one-api/common"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
)
|
||||
|
||||
type PriceData struct {
|
||||
ModelPrice float64
|
||||
ModelRatio float64
|
||||
CompletionRatio float64
|
||||
CacheRatio float64
|
||||
GroupRatio float64
|
||||
UsePrice bool
|
||||
ShouldPreConsumedQuota int
|
||||
}
|
||||
|
||||
func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
|
||||
modelPrice, usePrice := setting.GetModelPrice(info.OriginModelName, false)
|
||||
modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false)
|
||||
groupRatio := setting.GetGroupRatio(info.Group)
|
||||
var preConsumedQuota int
|
||||
var modelRatio float64
|
||||
var completionRatio float64
|
||||
var cacheRatio float64
|
||||
if !usePrice {
|
||||
preConsumedTokens := common.PreConsumedQuota
|
||||
if maxTokens != 0 {
|
||||
preConsumedTokens = promptTokens + maxTokens
|
||||
}
|
||||
var success bool
|
||||
modelRatio, success = setting.GetModelRatio(info.OriginModelName)
|
||||
modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName)
|
||||
if !success {
|
||||
if info.UserId == 1 {
|
||||
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
|
||||
@@ -37,7 +40,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
|
||||
}
|
||||
}
|
||||
completionRatio = setting.GetCompletionRatio(info.OriginModelName)
|
||||
completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName)
|
||||
cacheRatio, _ = operation_setting.GetCacheRatio(info.OriginModelName)
|
||||
ratio := modelRatio * groupRatio
|
||||
preConsumedQuota = int(float64(preConsumedTokens) * ratio)
|
||||
} else {
|
||||
@@ -49,6 +53,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
CompletionRatio: completionRatio,
|
||||
GroupRatio: groupRatio,
|
||||
UsePrice: usePrice,
|
||||
CacheRatio: cacheRatio,
|
||||
ShouldPreConsumedQuota: preConsumedQuota,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/relay/helper"
|
||||
@@ -80,10 +79,6 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
userQuota, err := model.GetUserQuota(relayInfo.UserId, false)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "get_user_quota_failed", http.StatusInternalServerError)
|
||||
}
|
||||
preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
|
||||
if openaiErr != nil {
|
||||
return openaiErr
|
||||
|
||||
+5
-4
@@ -15,6 +15,7 @@ import (
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -157,10 +158,10 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
||||
return service.MidjourneyErrorWrapper(constant.MjRequestError, "sour_base64_and_target_base64_is_required")
|
||||
}
|
||||
modelName := service.CoverActionToModelName(constant.MjActionSwapFace)
|
||||
modelPrice, success := setting.GetModelPrice(modelName, true)
|
||||
modelPrice, success := operation_setting.GetModelPrice(modelName, true)
|
||||
// 如果没有配置价格,则使用默认价格
|
||||
if !success {
|
||||
defaultPrice, ok := setting.GetDefaultModelRatioMap()[modelName]
|
||||
defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName]
|
||||
if !ok {
|
||||
modelPrice = 0.1
|
||||
} else {
|
||||
@@ -463,10 +464,10 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
||||
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||
|
||||
modelName := service.CoverActionToModelName(midjRequest.Action)
|
||||
modelPrice, success := setting.GetModelPrice(modelName, true)
|
||||
modelPrice, success := operation_setting.GetModelPrice(modelName, true)
|
||||
// 如果没有配置价格,则使用默认价格
|
||||
if !success {
|
||||
defaultPrice, ok := setting.GetDefaultModelRatioMap()[modelName]
|
||||
defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName]
|
||||
if !ok {
|
||||
modelPrice = 0.1
|
||||
} else {
|
||||
|
||||
+16
-11
@@ -110,6 +110,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// pre-consume quota 预消耗配额
|
||||
preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
|
||||
if openaiErr != nil {
|
||||
@@ -303,34 +304,38 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: relayInfo.PromptTokens,
|
||||
}
|
||||
extraContent += " ,(可能是请求出错)"
|
||||
extraContent += "(可能是请求出错)"
|
||||
}
|
||||
useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
|
||||
promptTokens := usage.PromptTokens
|
||||
cacheTokens := usage.PromptTokensDetails.CachedTokens
|
||||
completionTokens := usage.CompletionTokens
|
||||
modelName := relayInfo.OriginModelName
|
||||
|
||||
tokenName := ctx.GetString("token_name")
|
||||
completionRatio := setting.GetCompletionRatio(modelName)
|
||||
completionRatio := priceData.CompletionRatio
|
||||
cacheRatio := priceData.CacheRatio
|
||||
ratio := priceData.ModelRatio * priceData.GroupRatio
|
||||
modelRatio := priceData.ModelRatio
|
||||
groupRatio := priceData.GroupRatio
|
||||
modelPrice := priceData.ModelPrice
|
||||
usePrice := priceData.UsePrice
|
||||
|
||||
quota := 0
|
||||
quotaCalculate := 0.0
|
||||
if !priceData.UsePrice {
|
||||
quota = promptTokens + int(math.Round(float64(completionTokens)*completionRatio))
|
||||
quota = int(math.Round(float64(quota) * ratio))
|
||||
if ratio != 0 && quota <= 0 {
|
||||
quota = 1
|
||||
quotaCalculate = float64(promptTokens-cacheTokens) + float64(cacheTokens)*cacheRatio
|
||||
quotaCalculate += float64(completionTokens) * completionRatio
|
||||
quotaCalculate = quotaCalculate * ratio
|
||||
if ratio != 0 && quotaCalculate <= 0 {
|
||||
quotaCalculate = 1
|
||||
}
|
||||
} else {
|
||||
quota = int(modelPrice * common.QuotaPerUnit * groupRatio)
|
||||
quotaCalculate = modelPrice * common.QuotaPerUnit * groupRatio
|
||||
}
|
||||
quota := int(quotaCalculate)
|
||||
totalTokens := promptTokens + completionTokens
|
||||
|
||||
var logContent string
|
||||
if !usePrice {
|
||||
if !priceData.UsePrice {
|
||||
logContent = fmt.Sprintf("模型倍率 %.2f,补全倍率 %.2f,分组倍率 %.2f", modelRatio, completionRatio, groupRatio)
|
||||
} else {
|
||||
logContent = fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f", modelPrice, groupRatio)
|
||||
@@ -371,7 +376,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
if extraContent != "" {
|
||||
logContent += ", " + extraContent
|
||||
}
|
||||
other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, modelPrice)
|
||||
other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice)
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel,
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
|
||||
|
||||
|
||||
+3
-2
@@ -16,6 +16,7 @@ import (
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -37,9 +38,9 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
|
||||
}
|
||||
|
||||
modelName := service.CoverTaskActionToModelName(platform, relayInfo.Action)
|
||||
modelPrice, success := setting.GetModelPrice(modelName, true)
|
||||
modelPrice, success := operation_setting.GetModelPrice(modelName, true)
|
||||
if !success {
|
||||
defaultPrice, ok := setting.GetDefaultModelRatioMap()[modelName]
|
||||
defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName]
|
||||
if !ok {
|
||||
modelPrice = 0.1
|
||||
} else {
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ import (
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
)
|
||||
|
||||
func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
@@ -39,7 +40,7 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
|
||||
}
|
||||
}
|
||||
//relayInfo.UpstreamModelName = textRequest.Model
|
||||
modelPrice, getModelPriceSuccess := setting.GetModelPrice(relayInfo.UpstreamModelName, false)
|
||||
modelPrice, getModelPriceSuccess := operation_setting.GetModelPrice(relayInfo.UpstreamModelName, false)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
|
||||
var preConsumedQuota int
|
||||
@@ -65,7 +66,7 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
|
||||
//if realtimeEvent.Session.MaxResponseOutputTokens != 0 {
|
||||
// preConsumedTokens = promptTokens + int(realtimeEvent.Session.MaxResponseOutputTokens)
|
||||
//}
|
||||
modelRatio, _ = setting.GetModelRatio(relayInfo.UpstreamModelName)
|
||||
modelRatio, _ = operation_setting.GetModelRatio(relayInfo.UpstreamModelName)
|
||||
ratio = modelRatio * groupRatio
|
||||
preConsumedQuota = int(float64(preConsumedTokens) * ratio)
|
||||
} else {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio, modelPrice float64) map[string]interface{} {
|
||||
func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64,
|
||||
cacheTokens int, cacheRatio float64, modelPrice float64) map[string]interface{} {
|
||||
other := make(map[string]interface{})
|
||||
other["model_ratio"] = modelRatio
|
||||
other["group_ratio"] = groupRatio
|
||||
other["completion_ratio"] = completionRatio
|
||||
other["cache_tokens"] = cacheTokens
|
||||
other["cache_ratio"] = cacheRatio
|
||||
other["model_price"] = modelPrice
|
||||
other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli())
|
||||
if relayInfo.ReasoningEffort != "" {
|
||||
@@ -27,7 +31,7 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
|
||||
}
|
||||
|
||||
func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice float64) map[string]interface{} {
|
||||
info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, modelPrice)
|
||||
info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice)
|
||||
info["ws"] = true
|
||||
info["audio_input"] = usage.InputTokenDetails.AudioTokens
|
||||
info["audio_output"] = usage.OutputTokenDetails.AudioTokens
|
||||
@@ -39,7 +43,7 @@ func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us
|
||||
}
|
||||
|
||||
func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice float64) map[string]interface{} {
|
||||
info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, modelPrice)
|
||||
info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice)
|
||||
info["audio"] = true
|
||||
info["audio_input"] = usage.PromptTokensDetails.AudioTokens
|
||||
info["audio_output"] = usage.CompletionTokenDetails.AudioTokens
|
||||
|
||||
+18
-16
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"math"
|
||||
"one-api/common"
|
||||
constant2 "one-api/constant"
|
||||
"one-api/dto"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/relay/helper"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -38,21 +38,23 @@ func calculateAudioQuota(info QuotaInfo) int {
|
||||
return int(info.ModelPrice * common.QuotaPerUnit * info.GroupRatio)
|
||||
}
|
||||
|
||||
completionRatio := setting.GetCompletionRatio(info.ModelName)
|
||||
audioRatio := setting.GetAudioRatio(info.ModelName)
|
||||
audioCompletionRatio := setting.GetAudioCompletionRatio(info.ModelName)
|
||||
completionRatio := operation_setting.GetCompletionRatio(info.ModelName)
|
||||
audioRatio := operation_setting.GetAudioRatio(info.ModelName)
|
||||
audioCompletionRatio := operation_setting.GetAudioCompletionRatio(info.ModelName)
|
||||
ratio := info.GroupRatio * info.ModelRatio
|
||||
|
||||
quota := info.InputDetails.TextTokens + int(math.Round(float64(info.OutputDetails.TextTokens)*completionRatio))
|
||||
quota += int(math.Round(float64(info.InputDetails.AudioTokens)*audioRatio)) +
|
||||
int(math.Round(float64(info.OutputDetails.AudioTokens)*audioRatio*audioCompletionRatio))
|
||||
quota := 0.0
|
||||
quota += float64(info.InputDetails.TextTokens)
|
||||
quota += float64(info.OutputDetails.TextTokens) * completionRatio
|
||||
quota += float64(info.InputDetails.AudioTokens) * audioRatio
|
||||
quota += float64(info.OutputDetails.AudioTokens) * audioRatio * audioCompletionRatio
|
||||
|
||||
quota = int(math.Round(float64(quota) * ratio))
|
||||
quota = quota * ratio
|
||||
if ratio != 0 && quota <= 0 {
|
||||
quota = 1
|
||||
}
|
||||
|
||||
return quota
|
||||
return int(quota)
|
||||
}
|
||||
|
||||
func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage) error {
|
||||
@@ -75,7 +77,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
||||
audioInputTokens := usage.InputTokenDetails.AudioTokens
|
||||
audioOutTokens := usage.OutputTokenDetails.AudioTokens
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
modelRatio, _ := setting.GetModelRatio(modelName)
|
||||
modelRatio, _ := operation_setting.GetModelRatio(modelName)
|
||||
|
||||
quotaInfo := QuotaInfo{
|
||||
InputDetails: TokenDetails{
|
||||
@@ -122,9 +124,9 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
|
||||
audioOutTokens := usage.OutputTokenDetails.AudioTokens
|
||||
|
||||
tokenName := ctx.GetString("token_name")
|
||||
completionRatio := setting.GetCompletionRatio(modelName)
|
||||
audioRatio := setting.GetAudioRatio(relayInfo.OriginModelName)
|
||||
audioCompletionRatio := setting.GetAudioCompletionRatio(modelName)
|
||||
completionRatio := operation_setting.GetCompletionRatio(modelName)
|
||||
audioRatio := operation_setting.GetAudioRatio(relayInfo.OriginModelName)
|
||||
audioCompletionRatio := operation_setting.GetAudioCompletionRatio(modelName)
|
||||
|
||||
quotaInfo := QuotaInfo{
|
||||
InputDetails: TokenDetails{
|
||||
@@ -184,9 +186,9 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
audioOutTokens := usage.CompletionTokenDetails.AudioTokens
|
||||
|
||||
tokenName := ctx.GetString("token_name")
|
||||
completionRatio := setting.GetCompletionRatio(relayInfo.OriginModelName)
|
||||
audioRatio := setting.GetAudioRatio(relayInfo.OriginModelName)
|
||||
audioCompletionRatio := setting.GetAudioCompletionRatio(relayInfo.OriginModelName)
|
||||
completionRatio := operation_setting.GetCompletionRatio(relayInfo.OriginModelName)
|
||||
audioRatio := operation_setting.GetAudioRatio(relayInfo.OriginModelName)
|
||||
audioCompletionRatio := operation_setting.GetAudioCompletionRatio(relayInfo.OriginModelName)
|
||||
|
||||
modelRatio := priceData.ModelRatio
|
||||
groupRatio := priceData.GroupRatio
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -33,7 +33,7 @@ func InitTokenEncoders() {
|
||||
if err != nil {
|
||||
common.FatalLog(fmt.Sprintf("failed to get gpt-4o token encoder: %s", err.Error()))
|
||||
}
|
||||
for model, _ := range setting.GetDefaultModelRatioMap() {
|
||||
for model, _ := range operation_setting.GetDefaultModelRatioMap() {
|
||||
if strings.HasPrefix(model, "gpt-3.5") {
|
||||
tokenEncoderMap[model] = cl100TokenEncoder
|
||||
} else if strings.HasPrefix(model, "gpt-4") {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"one-api/common"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var defaultCacheRatio = map[string]float64{
|
||||
"gpt-4": 0.5,
|
||||
"o1-2024-12-17": 0.5,
|
||||
"o1-preview-2024-09-12": 0.5,
|
||||
"o1-mini-2024-09-12": 0.5,
|
||||
"gpt-4o-2024-11-20": 0.5,
|
||||
"gpt-4o-2024-08-06": 0.5,
|
||||
"gpt-4o-mini-2024-07-18": 0.5,
|
||||
"gpt-4o-realtime-preview": 0.5,
|
||||
"gpt-4o-mini-realtime-preview": 0.5,
|
||||
"deepseek-chat": 0.1,
|
||||
"deepseek-reasoner": 0.1,
|
||||
"deepseek-coder": 0.1,
|
||||
}
|
||||
|
||||
var defaultCreateCacheRatio = map[string]float64{}
|
||||
|
||||
var cacheRatioMap map[string]float64
|
||||
var cacheRatioMapMutex sync.RWMutex
|
||||
|
||||
// GetCacheRatioMap returns the cache ratio map
|
||||
func GetCacheRatioMap() map[string]float64 {
|
||||
cacheRatioMapMutex.Lock()
|
||||
defer cacheRatioMapMutex.Unlock()
|
||||
if cacheRatioMap == nil {
|
||||
cacheRatioMap = defaultCacheRatio
|
||||
}
|
||||
return cacheRatioMap
|
||||
}
|
||||
|
||||
// CacheRatio2JSONString converts the cache ratio map to a JSON string
|
||||
func CacheRatio2JSONString() string {
|
||||
GetCacheRatioMap()
|
||||
jsonBytes, err := json.Marshal(cacheRatioMap)
|
||||
if err != nil {
|
||||
common.SysError("error marshalling cache ratio: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
// UpdateCacheRatioByJSONString updates the cache ratio map from a JSON string
|
||||
func UpdateCacheRatioByJSONString(jsonStr string) error {
|
||||
cacheRatioMapMutex.Lock()
|
||||
defer cacheRatioMapMutex.Unlock()
|
||||
cacheRatioMap = make(map[string]float64)
|
||||
return json.Unmarshal([]byte(jsonStr), &cacheRatioMap)
|
||||
}
|
||||
|
||||
// GetCacheRatio returns the cache ratio for a model
|
||||
func GetCacheRatio(name string) (float64, bool) {
|
||||
GetCacheRatioMap()
|
||||
ratio, ok := cacheRatioMap[name]
|
||||
if !ok {
|
||||
return 1, false // Default to 0.5 if not found
|
||||
}
|
||||
return ratio, true
|
||||
}
|
||||
|
||||
// DefaultCacheRatio2JSONString converts the default cache ratio map to a JSON string
|
||||
func DefaultCacheRatio2JSONString() string {
|
||||
jsonBytes, err := json.Marshal(defaultCacheRatio)
|
||||
if err != nil {
|
||||
common.SysError("error marshalling default cache ratio: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
// GetDefaultCacheRatioMap returns the default cache ratio map
|
||||
func GetDefaultCacheRatioMap() map[string]float64 {
|
||||
return defaultCacheRatio
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
package setting
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"one-api/common"
|
||||
"one-api/setting/operation_setting"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -326,7 +325,7 @@ func GetModelRatio(name string) (float64, bool) {
|
||||
}
|
||||
ratio, ok := modelRatioMap[name]
|
||||
if !ok {
|
||||
return 37.5, operation_setting.SelfUseModeEnabled
|
||||
return 37.5, SelfUseModeEnabled
|
||||
}
|
||||
return ratio, true
|
||||
}
|
||||
@@ -29,10 +29,12 @@ import {
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
Typography,
|
||||
Checkbox,
|
||||
Layout
|
||||
} from '@douyinfe/semi-ui';
|
||||
import EditChannel from '../pages/Channel/EditChannel';
|
||||
import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
|
||||
import { IconList, IconTreeTriangleDown, IconClose, IconFilter, IconPlus, IconRefresh, IconSetting } from '@douyinfe/semi-icons';
|
||||
import { loadChannelModels } from './utils.js';
|
||||
import EditTagModal from '../pages/Channel/EditTagModal.js';
|
||||
import TextNumberInput from './custom/TextNumberInput.js';
|
||||
@@ -141,21 +143,105 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
// {
|
||||
// title: '',
|
||||
// dataIndex: 'checkbox',
|
||||
// className: 'checkbox',
|
||||
// },
|
||||
// Define column keys for selection
|
||||
const COLUMN_KEYS = {
|
||||
ID: 'id',
|
||||
NAME: 'name',
|
||||
GROUP: 'group',
|
||||
TYPE: 'type',
|
||||
STATUS: 'status',
|
||||
RESPONSE_TIME: 'response_time',
|
||||
BALANCE: 'balance',
|
||||
PRIORITY: 'priority',
|
||||
WEIGHT: 'weight',
|
||||
OPERATE: 'operate'
|
||||
};
|
||||
|
||||
// State for column visibility
|
||||
const [visibleColumns, setVisibleColumns] = useState({});
|
||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
||||
|
||||
// Load saved column preferences from localStorage
|
||||
useEffect(() => {
|
||||
const savedColumns = localStorage.getItem('channels-table-columns');
|
||||
if (savedColumns) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedColumns);
|
||||
// Make sure all columns are accounted for
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
const merged = { ...defaults, ...parsed };
|
||||
setVisibleColumns(merged);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved column preferences', e);
|
||||
initDefaultColumns();
|
||||
}
|
||||
} else {
|
||||
initDefaultColumns();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update table when column visibility changes
|
||||
useEffect(() => {
|
||||
if (Object.keys(visibleColumns).length > 0) {
|
||||
// Save to localStorage
|
||||
localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns));
|
||||
}
|
||||
}, [visibleColumns]);
|
||||
|
||||
// Get default column visibility
|
||||
const getDefaultColumnVisibility = () => {
|
||||
return {
|
||||
[COLUMN_KEYS.ID]: true,
|
||||
[COLUMN_KEYS.NAME]: true,
|
||||
[COLUMN_KEYS.GROUP]: true,
|
||||
[COLUMN_KEYS.TYPE]: true,
|
||||
[COLUMN_KEYS.STATUS]: true,
|
||||
[COLUMN_KEYS.RESPONSE_TIME]: true,
|
||||
[COLUMN_KEYS.BALANCE]: true,
|
||||
[COLUMN_KEYS.PRIORITY]: true,
|
||||
[COLUMN_KEYS.WEIGHT]: true,
|
||||
[COLUMN_KEYS.OPERATE]: true
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize default column visibility
|
||||
const initDefaultColumns = () => {
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
setVisibleColumns(defaults);
|
||||
};
|
||||
|
||||
// Handle column visibility change
|
||||
const handleColumnVisibilityChange = (columnKey, checked) => {
|
||||
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
|
||||
setVisibleColumns(updatedColumns);
|
||||
};
|
||||
|
||||
// Handle "Select All" checkbox
|
||||
const handleSelectAll = (checked) => {
|
||||
const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]);
|
||||
const updatedColumns = {};
|
||||
|
||||
allKeys.forEach(key => {
|
||||
updatedColumns[key] = checked;
|
||||
});
|
||||
|
||||
setVisibleColumns(updatedColumns);
|
||||
};
|
||||
|
||||
// Define all columns with keys
|
||||
const allColumns = [
|
||||
{
|
||||
key: COLUMN_KEYS.ID,
|
||||
title: t('ID'),
|
||||
dataIndex: 'id'
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.NAME,
|
||||
title: t('名称'),
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.GROUP,
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
render: (text, record, index) => {
|
||||
@@ -177,6 +263,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.TYPE,
|
||||
title: t('类型'),
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
@@ -188,6 +275,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.STATUS,
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
render: (text, record, index) => {
|
||||
@@ -211,6 +299,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.RESPONSE_TIME,
|
||||
title: t('响应时间'),
|
||||
dataIndex: 'response_time',
|
||||
render: (text, record, index) => {
|
||||
@@ -218,6 +307,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.BALANCE,
|
||||
title: t('已用/剩余'),
|
||||
dataIndex: 'expired_time',
|
||||
render: (text, record, index) => {
|
||||
@@ -255,6 +345,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.PRIORITY,
|
||||
title: t('优先级'),
|
||||
dataIndex: 'priority',
|
||||
render: (text, record, index) => {
|
||||
@@ -304,6 +395,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.WEIGHT,
|
||||
title: t('权重'),
|
||||
dataIndex: 'weight',
|
||||
render: (text, record, index) => {
|
||||
@@ -353,6 +445,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.OPERATE,
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
render: (text, record, index) => {
|
||||
@@ -493,6 +586,68 @@ const ChannelsTable = () => {
|
||||
}
|
||||
];
|
||||
|
||||
// Filter columns based on visibility settings
|
||||
const getVisibleColumns = () => {
|
||||
return allColumns.filter(column => visibleColumns[column.key]);
|
||||
};
|
||||
|
||||
// Column selector modal
|
||||
const renderColumnSelector = () => {
|
||||
return (
|
||||
<Modal
|
||||
title={t('列设置')}
|
||||
visible={showColumnSelector}
|
||||
onCancel={() => setShowColumnSelector(false)}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
|
||||
<Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button>
|
||||
<Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
|
||||
</>
|
||||
}
|
||||
style={{ width: 500 }}
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Checkbox
|
||||
checked={Object.values(visibleColumns).every(v => v === true)}
|
||||
indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)}
|
||||
onChange={e => handleSelectAll(e.target.checked)}
|
||||
>
|
||||
{t('全选')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '6px',
|
||||
padding: '16px'
|
||||
}}>
|
||||
{allColumns.map(column => {
|
||||
// Skip columns without title
|
||||
if (!column.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
|
||||
<Checkbox
|
||||
checked={!!visibleColumns[column.key]}
|
||||
onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
|
||||
>
|
||||
{column.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
@@ -1032,6 +1187,7 @@ const ChannelsTable = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderColumnSelector()}
|
||||
<EditTagModal
|
||||
visible={showEditTag}
|
||||
tag={editingTag}
|
||||
@@ -1238,15 +1394,22 @@ const ChannelsTable = () => {
|
||||
>
|
||||
{t('批量设置标签')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
type="tertiary"
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Table
|
||||
className={'channel-table'}
|
||||
style={{ marginTop: 15 }}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
columns={getVisibleColumns()}
|
||||
dataSource={pageData}
|
||||
pagination={{
|
||||
currentPage: activePage,
|
||||
@@ -1260,7 +1423,6 @@ const ChannelsTable = () => {
|
||||
},
|
||||
onPageChange: handlePageChange
|
||||
}}
|
||||
loading={loading}
|
||||
onRow={handleRow}
|
||||
rowSelection={
|
||||
enableBatchDelete
|
||||
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
Spin,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Checkbox
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
@@ -34,7 +35,7 @@ import {
|
||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||
import { getLogOther } from '../helpers/other.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { IconInherit, IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { IconInherit, IconRefresh, IconSetting } from '@douyinfe/semi-icons';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
@@ -215,12 +216,104 @@ const LogsTable = () => {
|
||||
|
||||
}
|
||||
|
||||
const columns = [
|
||||
// Define column keys for selection
|
||||
const COLUMN_KEYS = {
|
||||
TIME: 'time',
|
||||
CHANNEL: 'channel',
|
||||
USERNAME: 'username',
|
||||
TOKEN: 'token',
|
||||
GROUP: 'group',
|
||||
TYPE: 'type',
|
||||
MODEL: 'model',
|
||||
USE_TIME: 'use_time',
|
||||
PROMPT: 'prompt',
|
||||
COMPLETION: 'completion',
|
||||
COST: 'cost',
|
||||
RETRY: 'retry',
|
||||
DETAILS: 'details'
|
||||
};
|
||||
|
||||
// State for column visibility
|
||||
const [visibleColumns, setVisibleColumns] = useState({});
|
||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
||||
|
||||
// Load saved column preferences from localStorage
|
||||
useEffect(() => {
|
||||
const savedColumns = localStorage.getItem('logs-table-columns');
|
||||
if (savedColumns) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedColumns);
|
||||
// Make sure all columns are accounted for
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
const merged = { ...defaults, ...parsed };
|
||||
setVisibleColumns(merged);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved column preferences', e);
|
||||
initDefaultColumns();
|
||||
}
|
||||
} else {
|
||||
initDefaultColumns();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get default column visibility based on user role
|
||||
const getDefaultColumnVisibility = () => {
|
||||
return {
|
||||
[COLUMN_KEYS.TIME]: true,
|
||||
[COLUMN_KEYS.CHANNEL]: isAdminUser,
|
||||
[COLUMN_KEYS.USERNAME]: isAdminUser,
|
||||
[COLUMN_KEYS.TOKEN]: true,
|
||||
[COLUMN_KEYS.GROUP]: true,
|
||||
[COLUMN_KEYS.TYPE]: true,
|
||||
[COLUMN_KEYS.MODEL]: true,
|
||||
[COLUMN_KEYS.USE_TIME]: true,
|
||||
[COLUMN_KEYS.PROMPT]: true,
|
||||
[COLUMN_KEYS.COMPLETION]: true,
|
||||
[COLUMN_KEYS.COST]: true,
|
||||
[COLUMN_KEYS.RETRY]: isAdminUser,
|
||||
[COLUMN_KEYS.DETAILS]: true
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize default column visibility
|
||||
const initDefaultColumns = () => {
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
setVisibleColumns(defaults);
|
||||
localStorage.setItem('logs-table-columns', JSON.stringify(defaults));
|
||||
};
|
||||
|
||||
// Handle column visibility change
|
||||
const handleColumnVisibilityChange = (columnKey, checked) => {
|
||||
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
|
||||
setVisibleColumns(updatedColumns);
|
||||
};
|
||||
|
||||
// Handle "Select All" checkbox
|
||||
const handleSelectAll = (checked) => {
|
||||
const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]);
|
||||
const updatedColumns = {};
|
||||
|
||||
allKeys.forEach(key => {
|
||||
// For admin-only columns, only enable them if user is admin
|
||||
if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.RETRY) && !isAdminUser) {
|
||||
updatedColumns[key] = false;
|
||||
} else {
|
||||
updatedColumns[key] = checked;
|
||||
}
|
||||
});
|
||||
|
||||
setVisibleColumns(updatedColumns);
|
||||
};
|
||||
|
||||
// Define all columns
|
||||
const allColumns = [
|
||||
{
|
||||
key: COLUMN_KEYS.TIME,
|
||||
title: t('时间'),
|
||||
dataIndex: 'timestamp2string',
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.CHANNEL,
|
||||
title: t('渠道'),
|
||||
dataIndex: 'channel',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
@@ -249,6 +342,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.USERNAME,
|
||||
title: t('用户'),
|
||||
dataIndex: 'username',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
@@ -274,6 +368,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.TOKEN,
|
||||
title: t('令牌'),
|
||||
dataIndex: 'token_name',
|
||||
render: (text, record, index) => {
|
||||
@@ -297,6 +392,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.GROUP,
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
render: (text, record, index) => {
|
||||
@@ -333,6 +429,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.TYPE,
|
||||
title: t('类型'),
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
@@ -340,6 +437,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.MODEL,
|
||||
title: t('模型'),
|
||||
dataIndex: 'model_name',
|
||||
render: (text, record, index) => {
|
||||
@@ -351,6 +449,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.USE_TIME,
|
||||
title: t('用时/首字'),
|
||||
dataIndex: 'use_time',
|
||||
render: (text, record, index) => {
|
||||
@@ -378,6 +477,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.PROMPT,
|
||||
title: t('提示'),
|
||||
dataIndex: 'prompt_tokens',
|
||||
render: (text, record, index) => {
|
||||
@@ -389,6 +489,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.COMPLETION,
|
||||
title: t('补全'),
|
||||
dataIndex: 'completion_tokens',
|
||||
render: (text, record, index) => {
|
||||
@@ -401,6 +502,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.COST,
|
||||
title: t('花费'),
|
||||
dataIndex: 'quota',
|
||||
render: (text, record, index) => {
|
||||
@@ -412,6 +514,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.RETRY,
|
||||
title: t('重试'),
|
||||
dataIndex: 'retry',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
@@ -439,6 +542,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.DETAILS,
|
||||
title: t('详情'),
|
||||
dataIndex: 'content',
|
||||
render: (text, record, index) => {
|
||||
@@ -464,6 +568,8 @@ const LogsTable = () => {
|
||||
other.model_ratio,
|
||||
other.model_price,
|
||||
other.group_ratio,
|
||||
other.cache_tokens || 0,
|
||||
other.cache_ratio || 1.0,
|
||||
);
|
||||
return (
|
||||
<Paragraph
|
||||
@@ -479,6 +585,76 @@ const LogsTable = () => {
|
||||
},
|
||||
];
|
||||
|
||||
// Update table when column visibility changes
|
||||
useEffect(() => {
|
||||
if (Object.keys(visibleColumns).length > 0) {
|
||||
// Save to localStorage
|
||||
localStorage.setItem('logs-table-columns', JSON.stringify(visibleColumns));
|
||||
}
|
||||
}, [visibleColumns]);
|
||||
|
||||
// Filter columns based on visibility settings
|
||||
const getVisibleColumns = () => {
|
||||
return allColumns.filter(column => visibleColumns[column.key]);
|
||||
};
|
||||
|
||||
// Column selector modal
|
||||
const renderColumnSelector = () => {
|
||||
return (
|
||||
<Modal
|
||||
title={t('列设置')}
|
||||
visible={showColumnSelector}
|
||||
onCancel={() => setShowColumnSelector(false)}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
|
||||
<Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button>
|
||||
<Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Checkbox
|
||||
checked={Object.values(visibleColumns).every(v => v === true)}
|
||||
indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)}
|
||||
onChange={e => handleSelectAll(e.target.checked)}
|
||||
>
|
||||
{t('全选')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '6px',
|
||||
padding: '16px'
|
||||
}}>
|
||||
{allColumns.map(column => {
|
||||
// Skip admin-only columns for non-admin users
|
||||
if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL ||
|
||||
column.key === COLUMN_KEYS.USERNAME ||
|
||||
column.key === COLUMN_KEYS.RETRY)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
|
||||
<Checkbox
|
||||
checked={!!visibleColumns[column.key]}
|
||||
onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
|
||||
>
|
||||
{column.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [expandData, setExpandData] = useState({});
|
||||
@@ -636,6 +812,12 @@ const LogsTable = () => {
|
||||
value: other.text_output,
|
||||
});
|
||||
}
|
||||
if (other?.cache_tokens > 0) {
|
||||
expandDataLocal.push({
|
||||
key: t('缓存 Tokens'),
|
||||
value: other.cache_tokens,
|
||||
});
|
||||
}
|
||||
expandDataLocal.push({
|
||||
key: t('日志详情'),
|
||||
value: logs[i].content,
|
||||
@@ -665,6 +847,8 @@ const LogsTable = () => {
|
||||
other?.audio_ratio,
|
||||
other?.audio_completion_ratio,
|
||||
other.group_ratio,
|
||||
other.cache_tokens || 0,
|
||||
other.cache_ratio || 1.0,
|
||||
);
|
||||
} else {
|
||||
content = renderModelPrice(
|
||||
@@ -674,6 +858,8 @@ const LogsTable = () => {
|
||||
other.model_price,
|
||||
other.completion_ratio,
|
||||
other.group_ratio,
|
||||
other.cache_tokens || 0,
|
||||
other.cache_ratio || 1.0,
|
||||
);
|
||||
}
|
||||
expandDataLocal.push({
|
||||
@@ -770,8 +956,9 @@ const LogsTable = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderColumnSelector()}
|
||||
<Layout>
|
||||
<Header>
|
||||
<Header style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
|
||||
<Spin spinning={loadingStat}>
|
||||
<Space>
|
||||
<Tag color='green' size='large' style={{ padding: 15 }}>
|
||||
@@ -905,10 +1092,19 @@ const LogsTable = () => {
|
||||
<Select.Option value='3'>{t('管理')}</Select.Option>
|
||||
<Select.Option value='4'>{t('系统')}</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
style={{ marginTop: 5 }}
|
||||
columns={columns}
|
||||
columns={getVisibleColumns()}
|
||||
expandedRowRender={expandRowRender}
|
||||
expandRowByClick={true}
|
||||
dataSource={logs}
|
||||
|
||||
@@ -28,6 +28,7 @@ const OperationSetting = () => {
|
||||
PreConsumedQuota: 0,
|
||||
StreamCacheQueueLength: 0,
|
||||
ModelRatio: '',
|
||||
CacheRatio: '',
|
||||
CompletionRatio: '',
|
||||
ModelPrice: '',
|
||||
GroupRatio: '',
|
||||
@@ -77,7 +78,8 @@ const OperationSetting = () => {
|
||||
item.key === 'GroupRatio' ||
|
||||
item.key === 'UserUsableGroups' ||
|
||||
item.key === 'CompletionRatio' ||
|
||||
item.key === 'ModelPrice'
|
||||
item.key === 'ModelPrice' ||
|
||||
item.key === 'CacheRatio'
|
||||
) {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
|
||||
@@ -294,39 +294,36 @@ const SiderBar = () => {
|
||||
}}
|
||||
>
|
||||
{/* Chat Section - Only show if there are chat items */}
|
||||
{chatItems.length > 0 && (
|
||||
<>
|
||||
{chatMenuItems.map((item) => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
return (
|
||||
<Nav.Sub
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
>
|
||||
{item.items.map((subItem) => (
|
||||
<Nav.Item
|
||||
key={subItem.itemKey}
|
||||
itemKey={subItem.itemKey}
|
||||
text={subItem.text}
|
||||
/>
|
||||
))}
|
||||
</Nav.Sub>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
{chatMenuItems.map((item) => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
return (
|
||||
<Nav.Sub
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
>
|
||||
{item.items.map((subItem) => (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
key={subItem.itemKey}
|
||||
itemKey={subItem.itemKey}
|
||||
text={subItem.text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
</Nav.Sub>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
|
||||
{/* Divider */}
|
||||
<Divider style={dividerStyle} />
|
||||
|
||||
+83
-20
@@ -298,6 +298,8 @@ export function renderModelPrice(
|
||||
modelPrice = -1,
|
||||
completionRatio,
|
||||
groupRatio,
|
||||
cacheTokens = 0,
|
||||
cacheRatio = 1.0,
|
||||
) {
|
||||
if (modelPrice !== -1) {
|
||||
return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
|
||||
@@ -311,9 +313,15 @@ export function renderModelPrice(
|
||||
}
|
||||
let inputRatioPrice = modelRatio * 2.0;
|
||||
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
|
||||
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
|
||||
|
||||
// Calculate effective input tokens (non-cached + cached with ratio applied)
|
||||
const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
|
||||
|
||||
let price =
|
||||
(inputTokens / 1000000) * inputRatioPrice * groupRatio +
|
||||
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
||||
(completionTokens / 1000000) * completionRatioPrice * groupRatio;
|
||||
|
||||
return (
|
||||
<>
|
||||
<article>
|
||||
@@ -327,16 +335,36 @@ export function renderModelPrice(
|
||||
ratio: groupRatio,
|
||||
total: completionRatioPrice * groupRatio
|
||||
})}</p>
|
||||
{cacheTokens > 0 && (
|
||||
<p>{i18next.t('缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
|
||||
price: cacheRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: cacheRatioPrice * groupRatio,
|
||||
cacheRatio: cacheRatio
|
||||
})}</p>
|
||||
)}
|
||||
<p></p>
|
||||
<p>
|
||||
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6)
|
||||
})}
|
||||
{cacheTokens > 0 ?
|
||||
i18next.t('提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6)
|
||||
}) :
|
||||
i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
@@ -349,6 +377,8 @@ export function renderModelPriceSimple(
|
||||
modelRatio,
|
||||
modelPrice = -1,
|
||||
groupRatio,
|
||||
cacheTokens = 0,
|
||||
cacheRatio = 1.0,
|
||||
) {
|
||||
if (modelPrice !== -1) {
|
||||
return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
|
||||
@@ -356,10 +386,18 @@ export function renderModelPriceSimple(
|
||||
ratio: groupRatio
|
||||
});
|
||||
} else {
|
||||
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
|
||||
ratio: modelRatio,
|
||||
groupRatio: groupRatio
|
||||
});
|
||||
if (cacheTokens !== 0) {
|
||||
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}} * 缓存倍率: {{cacheRatio}}', {
|
||||
ratio: modelRatio,
|
||||
groupRatio: groupRatio,
|
||||
cacheRatio: cacheRatio
|
||||
});
|
||||
} else {
|
||||
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
|
||||
ratio: modelRatio,
|
||||
groupRatio: groupRatio
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +412,8 @@ export function renderAudioModelPrice(
|
||||
audioRatio,
|
||||
audioCompletionRatio,
|
||||
groupRatio,
|
||||
cacheTokens = 0,
|
||||
cacheRatio = 1.0,
|
||||
) {
|
||||
// 1 ratio = $0.002 / 1K tokens
|
||||
if (modelPrice !== -1) {
|
||||
@@ -388,8 +428,13 @@ export function renderAudioModelPrice(
|
||||
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
||||
let inputRatioPrice = modelRatio * 2.0;
|
||||
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
|
||||
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
|
||||
|
||||
// Calculate effective input tokens (non-cached + cached with ratio applied)
|
||||
const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
|
||||
|
||||
let price =
|
||||
(inputTokens / 1000000) * inputRatioPrice * groupRatio +
|
||||
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
||||
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
|
||||
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
|
||||
(audioCompletionTokens / 1000000) * inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio;
|
||||
@@ -406,6 +451,14 @@ export function renderAudioModelPrice(
|
||||
ratio: groupRatio,
|
||||
total: completionRatioPrice * groupRatio
|
||||
})}</p>
|
||||
{cacheTokens > 0 && (
|
||||
<p>{i18next.t('缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
|
||||
price: cacheRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: cacheRatioPrice * groupRatio,
|
||||
cacheRatio: cacheRatio
|
||||
})}</p>
|
||||
)}
|
||||
<p>{i18next.t('音频提示:${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens', {
|
||||
price: inputRatioPrice,
|
||||
ratio: groupRatio,
|
||||
@@ -420,12 +473,22 @@ export function renderAudioModelPrice(
|
||||
total: inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio
|
||||
})}</p>
|
||||
<p>
|
||||
{i18next.t('文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice
|
||||
})}
|
||||
{cacheTokens > 0 ?
|
||||
i18next.t('文字提示 {{nonCacheInput}} tokens + 文字缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice
|
||||
}) :
|
||||
i18next.t('文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice
|
||||
})
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t('音频提示 {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + 音频补全 {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}', {
|
||||
|
||||
@@ -1338,5 +1338,9 @@
|
||||
"0.1-1之间的小数": "Decimal between 0.1 and 1",
|
||||
"模型相关设置": "Model related settings",
|
||||
"收起侧边栏": "Collapse sidebar",
|
||||
"展开侧边栏": "Expand sidebar"
|
||||
"展开侧边栏": "Expand sidebar",
|
||||
"提示缓存倍率": "Prompt cache ratio",
|
||||
"缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (cache ratio: {{cacheRatio}})",
|
||||
"提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}",
|
||||
"缓存 Tokens": "Cache Tokens"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function ModelRatioSettings(props) {
|
||||
const [inputs, setInputs] = useState({
|
||||
ModelPrice: '',
|
||||
ModelRatio: '',
|
||||
CacheRatio: '',
|
||||
CompletionRatio: '',
|
||||
});
|
||||
const refForm = useRef();
|
||||
@@ -139,6 +140,25 @@ export default function ModelRatioSettings(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.TextArea
|
||||
label={t('提示缓存倍率')}
|
||||
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
|
||||
field={'CacheRatio'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串'
|
||||
}
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, CacheRatio: value })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.TextArea
|
||||
|
||||
Reference in New Issue
Block a user