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 工具计费
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,

View File

@@ -618,7 +618,6 @@ const LogsTable = () => {
</Paragraph>
);
}
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({

View File

@@ -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(
)}
</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>
{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),
},
)}
</p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article>
@@ -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,
},
);
}
}
}