From c0095d4521b7b96d484467d43271fd4d7a79ccc5 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Wed, 7 May 2025 01:08:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20built=20in=20tools?= =?UTF-8?q?=20=E8=AE=A1=E8=B4=B9=E5=89=8D=E7=AB=AF=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/relay-text.go | 12 ++- web/src/components/LogsTable.js | 14 ++- web/src/helpers/render.js | 180 ++++++++++++++++++++++++-------- 3 files changed, 156 insertions(+), 50 deletions(-) diff --git a/relay/relay-text.go b/relay/relay-text.go index d2b2c208..89a6a973 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -361,11 +361,12 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, // openai web search 工具计费 var dWebSearchQuota decimal.Decimal + var webSearchPrice float64 if relayInfo.ResponsesUsageInfo != nil { if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 { - priceWebSearchPerThousandCalls := operation_setting.GetWebSearchPricePerThousand(modelName, webSearchTool.SearchContextSize) // 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000) - dWebSearchQuota = decimal.NewFromFloat(priceWebSearchPerThousandCalls). + webSearchPrice = operation_setting.GetWebSearchPricePerThousand(modelName, webSearchTool.SearchContextSize) + dWebSearchQuota = decimal.NewFromFloat(webSearchPrice). Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))). Div(decimal.NewFromInt(1000)) extraContent += fmt.Sprintf("Web Search 调用 %d 次,上下文大小 %s,调用花费 $%s", @@ -374,9 +375,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, } // file search tool 计费 var dFileSearchQuota decimal.Decimal + var fileSearchPrice float64 if relayInfo.ResponsesUsageInfo != nil { if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists && fileSearchTool.CallCount > 0 { - dFileSearchQuota = decimal.NewFromFloat(operation_setting.GetFileSearchPricePerThousand()). + fileSearchPrice = operation_setting.GetFileSearchPricePerThousand() + dFileSearchQuota = decimal.NewFromFloat(fileSearchPrice). Mul(decimal.NewFromInt(int64(fileSearchTool.CallCount))). Div(decimal.NewFromInt(1000)) extraContent += fmt.Sprintf("File Search 调用 %d 次,调用花费 $%s", @@ -463,13 +466,14 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists { other["web_search"] = true other["web_search_call_count"] = webSearchTool.CallCount - other["web_search_context_size"] = webSearchTool.SearchContextSize + other["web_search_price"] = webSearchPrice } } if !dFileSearchQuota.IsZero() && relayInfo.ResponsesUsageInfo != nil { if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists { other["file_search"] = true other["file_search_call_count"] = fileSearchTool.CallCount + other["file_search_price"] = fileSearchPrice } } model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel, diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index 903677eb..6cf7e844 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -618,7 +618,6 @@ const LogsTable = () => { ); } - let content = other?.claude ? renderClaudeModelPriceSimple( other.model_ratio, @@ -935,6 +934,13 @@ const LogsTable = () => { other.model_price, other.group_ratio, other?.user_group_ratio, + false, + 1.0, + undefined, + other.web_search || false, + other.web_search_call_count || 0, + other.file_search || false, + other.file_search_call_count || 0, ), }); } @@ -995,6 +1001,12 @@ const LogsTable = () => { other?.image || false, other?.image_ratio || 0, other?.image_output || 0, + other?.web_search || false, + other?.web_search_call_count || 0, + other?.web_search_price || 0, + other?.file_search || false, + other?.file_search_call_count || 0, + other?.file_search_price || 0, ); } expandDataLocal.push({ diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 7b80da6f..fb4c3dbd 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -317,6 +317,12 @@ export function renderModelPrice( image = false, imageRatio = 1.0, imageOutputTokens = 0, + webSearch = false, + webSearchCallCount = 0, + webSearchPrice = 0, + fileSearch = false, + fileSearchCallCount = 0, + fileSearchPrice = 0, ) { if (modelPrice !== -1) { return i18next.t( @@ -339,14 +345,17 @@ export function renderModelPrice( // Calculate effective input tokens (non-cached + cached with ratio applied) let effectiveInputTokens = inputTokens - cacheTokens + cacheTokens * cacheRatio; -// Handle image tokens if present + // Handle image tokens if present if (image && imageOutputTokens > 0) { - effectiveInputTokens = inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; + effectiveInputTokens = + inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; } let price = (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + - (completionTokens / 1000000) * completionRatioPrice * groupRatio; + (completionTokens / 1000000) * completionRatioPrice * groupRatio + + (webSearchCallCount / 1000) * webSearchPrice + + (fileSearchCallCount / 1000) * fileSearchPrice; return ( <> @@ -391,9 +400,23 @@ export function renderModelPrice( )}

)} + {webSearch && webSearchCallCount > 0 && ( +

+ {i18next.t('Web搜索价格:${{price}} / 1K 次', { + price: webSearchPrice, + })} +

+ )} + {fileSearch && fileSearchCallCount > 0 && ( +

+ {i18next.t('文件搜索价格:${{price}} / 1K 次', { + price: fileSearchPrice, + })} +

+ )}

- {cacheTokens > 0 && !image + {cacheTokens > 0 && !image && !webSearch && !fileSearch ? i18next.t( '输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', { @@ -407,31 +430,75 @@ export function renderModelPrice( total: price.toFixed(6), }, ) - : image && imageOutputTokens > 0 - ? i18next.t( - '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', - { - nonImageInput: inputTokens - imageOutputTokens, - imageInput: imageOutputTokens, - imageRatio: imageRatio, - 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), - }, - )} + : image && imageOutputTokens > 0 && !webSearch && !fileSearch + ? i18next.t( + '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', + { + nonImageInput: inputTokens - imageOutputTokens, + imageInput: imageOutputTokens, + imageRatio: imageRatio, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + total: price.toFixed(6), + }, + ) + : webSearch && webSearchCallCount > 0 && !image && !fileSearch + ? i18next.t( + '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + webSearchCallCount, + webSearchPrice, + total: price.toFixed(6), + }, + ) + : fileSearch && fileSearchCallCount > 0 && !image && !webSearch + ? i18next.t( + '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + fileSearchCallCount, + fileSearchPrice, + total: price.toFixed(6), + }, + ) + : webSearch && webSearchCallCount > 0 && fileSearch && fileSearchCallCount > 0 && !image + ? i18next.t( + '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + webSearchCallCount, + webSearchPrice, + fileSearchCallCount, + fileSearchPrice, + 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), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -448,33 +515,56 @@ export function renderLogContent( user_group_ratio, image = false, imageRatio = 1.0, - useUserGroupRatio = undefined + useUserGroupRatio = undefined, + webSearch = false, + webSearchCallCount = 0, + fileSearch = false, + fileSearchCallCount = 0, ) { - const ratioLabel = useUserGroupRatio ? i18next.t('专属倍率') : i18next.t('分组倍率'); + const ratioLabel = useUserGroupRatio + ? i18next.t('专属倍率') + : i18next.t('分组倍率'); const ratio = useUserGroupRatio ? user_group_ratio : groupRatio; if (modelPrice !== -1) { return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { price: modelPrice, ratioType: ratioLabel, - ratio + ratio, }); } else { if (image) { - return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', { - modelRatio: modelRatio, - completionRatio: completionRatio, - imageRatio: imageRatio, - ratioType: ratioLabel, - ratio - }); + return i18next.t( + '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', + { + modelRatio: modelRatio, + completionRatio: completionRatio, + imageRatio: imageRatio, + ratioType: ratioLabel, + ratio, + }, + ); + } else if (webSearch) { + return i18next.t( + '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次', + { + modelRatio: modelRatio, + completionRatio: completionRatio, + ratioType: ratioLabel, + ratio, + webSearchCallCount, + }, + ); } else { - return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', { - modelRatio: modelRatio, - completionRatio: completionRatio, - ratioType: ratioLabel, - ratio - }); + return i18next.t( + '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', + { + modelRatio: modelRatio, + completionRatio: completionRatio, + ratioType: ratioLabel, + ratio, + }, + ); } } }