feat: 添加 built in tools 计费前端显示

This commit is contained in:
creamlike1024
2025-05-07 01:08:20 +08:00
parent 5043075135
commit c0095d4521
3 changed files with 156 additions and 50 deletions

View File

@@ -361,11 +361,12 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
// openai web search 工具计费 // openai web search 工具计费
var dWebSearchQuota decimal.Decimal var dWebSearchQuota decimal.Decimal
var webSearchPrice float64
if relayInfo.ResponsesUsageInfo != nil { if relayInfo.ResponsesUsageInfo != nil {
if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 { if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 {
priceWebSearchPerThousandCalls := operation_setting.GetWebSearchPricePerThousand(modelName, webSearchTool.SearchContextSize)
// 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000) // 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000)
dWebSearchQuota = decimal.NewFromFloat(priceWebSearchPerThousandCalls). webSearchPrice = operation_setting.GetWebSearchPricePerThousand(modelName, webSearchTool.SearchContextSize)
dWebSearchQuota = decimal.NewFromFloat(webSearchPrice).
Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))). Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))).
Div(decimal.NewFromInt(1000)) Div(decimal.NewFromInt(1000))
extraContent += fmt.Sprintf("Web Search 调用 %d 次,上下文大小 %s调用花费 $%s", extraContent += fmt.Sprintf("Web Search 调用 %d 次,上下文大小 %s调用花费 $%s",
@@ -374,9 +375,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
} }
// file search tool 计费 // file search tool 计费
var dFileSearchQuota decimal.Decimal var dFileSearchQuota decimal.Decimal
var fileSearchPrice float64
if relayInfo.ResponsesUsageInfo != nil { if relayInfo.ResponsesUsageInfo != nil {
if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists && fileSearchTool.CallCount > 0 { 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))). Mul(decimal.NewFromInt(int64(fileSearchTool.CallCount))).
Div(decimal.NewFromInt(1000)) Div(decimal.NewFromInt(1000))
extraContent += fmt.Sprintf("File Search 调用 %d 次,调用花费 $%s", 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 { if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists {
other["web_search"] = true other["web_search"] = true
other["web_search_call_count"] = webSearchTool.CallCount 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 !dFileSearchQuota.IsZero() && relayInfo.ResponsesUsageInfo != nil {
if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists { if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists {
other["file_search"] = true other["file_search"] = true
other["file_search_call_count"] = fileSearchTool.CallCount other["file_search_call_count"] = fileSearchTool.CallCount
other["file_search_price"] = fileSearchPrice
} }
} }
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel, model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel,

View File

@@ -618,7 +618,6 @@ const LogsTable = () => {
</Paragraph> </Paragraph>
); );
} }
let content = other?.claude let content = other?.claude
? renderClaudeModelPriceSimple( ? renderClaudeModelPriceSimple(
other.model_ratio, other.model_ratio,
@@ -935,6 +934,13 @@ const LogsTable = () => {
other.model_price, other.model_price,
other.group_ratio, other.group_ratio,
other?.user_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 || false,
other?.image_ratio || 0, other?.image_ratio || 0,
other?.image_output || 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({ expandDataLocal.push({

View File

@@ -317,6 +317,12 @@ export function renderModelPrice(
image = false, image = false,
imageRatio = 1.0, imageRatio = 1.0,
imageOutputTokens = 0, imageOutputTokens = 0,
webSearch = false,
webSearchCallCount = 0,
webSearchPrice = 0,
fileSearch = false,
fileSearchCallCount = 0,
fileSearchPrice = 0,
) { ) {
if (modelPrice !== -1) { if (modelPrice !== -1) {
return i18next.t( return i18next.t(
@@ -339,14 +345,17 @@ export function renderModelPrice(
// Calculate effective input tokens (non-cached + cached with ratio applied) // Calculate effective input tokens (non-cached + cached with ratio applied)
let effectiveInputTokens = let effectiveInputTokens =
inputTokens - cacheTokens + cacheTokens * cacheRatio; inputTokens - cacheTokens + cacheTokens * cacheRatio;
// Handle image tokens if present // Handle image tokens if present
if (image && imageOutputTokens > 0) { if (image && imageOutputTokens > 0) {
effectiveInputTokens = inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; effectiveInputTokens =
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
} }
let price = let price =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
(completionTokens / 1000000) * completionRatioPrice * groupRatio; (completionTokens / 1000000) * completionRatioPrice * groupRatio +
(webSearchCallCount / 1000) * webSearchPrice +
(fileSearchCallCount / 1000) * fileSearchPrice;
return ( return (
<> <>
@@ -391,9 +400,23 @@ export function renderModelPrice(
)} )}
</p> </p>
)} )}
{webSearch && webSearchCallCount > 0 && (
<p>
{i18next.t('Web搜索价格${{price}} / 1K 次', {
price: webSearchPrice,
})}
</p>
)}
{fileSearch && fileSearchCallCount > 0 && (
<p>
{i18next.t('文件搜索价格:${{price}} / 1K 次', {
price: fileSearchPrice,
})}
</p>
)}
<p></p> <p></p>
<p> <p>
{cacheTokens > 0 && !image {cacheTokens > 0 && !image && !webSearch && !fileSearch
? i18next.t( ? i18next.t(
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', '输入 {{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), total: price.toFixed(6),
}, },
) )
: image && imageOutputTokens > 0 : image && imageOutputTokens > 0 && !webSearch && !fileSearch
? i18next.t( ? i18next.t(
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
{ {
nonImageInput: inputTokens - imageOutputTokens, nonImageInput: inputTokens - imageOutputTokens,
imageInput: imageOutputTokens, imageInput: imageOutputTokens,
imageRatio: imageRatio, imageRatio: imageRatio,
price: inputRatioPrice, price: inputRatioPrice,
completion: completionTokens, completion: completionTokens,
compPrice: completionRatioPrice, compPrice: completionRatioPrice,
ratio: groupRatio, ratio: groupRatio,
total: price.toFixed(6), total: price.toFixed(6),
}, },
) )
: i18next.t( : webSearch && webSearchCallCount > 0 && !image && !fileSearch
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', ? i18next.t(
{ '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} = ${{total}}',
input: inputTokens, {
price: inputRatioPrice, input: inputTokens,
completion: completionTokens, price: inputRatioPrice,
compPrice: completionRatioPrice, completion: completionTokens,
ratio: groupRatio, compPrice: completionRatioPrice,
total: price.toFixed(6), 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),
},
)}
</p> </p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p> <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article> </article>
@@ -448,33 +515,56 @@ export function renderLogContent(
user_group_ratio, user_group_ratio,
image = false, image = false,
imageRatio = 1.0, 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; const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
if (modelPrice !== -1) { if (modelPrice !== -1) {
return i18next.t('模型价格 ${{price}}{{ratioType}} {{ratio}}', { return i18next.t('模型价格 ${{price}}{{ratioType}} {{ratio}}', {
price: modelPrice, price: modelPrice,
ratioType: ratioLabel, ratioType: ratioLabel,
ratio ratio,
}); });
} else { } else {
if (image) { if (image) {
return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}}{{ratioType}} {{ratio}}', { return i18next.t(
modelRatio: modelRatio, '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}}{{ratioType}} {{ratio}}',
completionRatio: completionRatio, {
imageRatio: imageRatio, modelRatio: modelRatio,
ratioType: ratioLabel, completionRatio: completionRatio,
ratio 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 { } else {
return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}}{{ratioType}} {{ratio}}', { return i18next.t(
modelRatio: modelRatio, '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}}{{ratioType}} {{ratio}}',
completionRatio: completionRatio, {
ratioType: ratioLabel, modelRatio: modelRatio,
ratio completionRatio: completionRatio,
}); ratioType: ratioLabel,
ratio,
},
);
} }
} }
} }