Skip to content

feat: add configurable current-request tool billing#6227

Open
bddiudiu wants to merge 1 commit into
QuantumNous:mainfrom
bddiudiu:current-request-tool-billing
Open

feat: add configurable current-request tool billing#6227
bddiudiu wants to merge 1 commit into
QuantumNous:mainfrom
bddiudiu:current-request-tool-billing

Conversation

@bddiudiu

@bddiudiu bddiudiu commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

针对于火山/百炼的response api请求, 实现工具调用的计费, 原平台中只能识别web_search_preview的工具调用计费.

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

image image

Summary by CodeRabbit

  • New Features
    • Added detailed per-tool usage tracking for supported tools.
    • Usage logs now display individual tool call counts and applicable prices.
    • Added pricing support for Web Extractor and Code Interpreter tools.
    • Tool pricing settings now retain default entries when saved configurations omit them.
  • Bug Fixes
    • Billing now counts only tool calls confirmed by the upstream provider.
    • Provider-reported usage totals take precedence over provisional call counts.
  • Documentation
    • Updated billing guidance and translations across supported languages.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

OpenAI Responses tool usage is now propagated from provider responses into built-in tool accounting, quota surcharges, configurable tool pricing, billing payloads, and per-tool usage-log displays.

Changes

Tool usage billing

Layer / File(s) Summary
Responses tool accounting
dto/openai_response.go, relay/common/relay_info.go, relay/channel/openai/...
Responses usage carries tool_usage; handlers record output tool calls and apply provider-supplied final counts, with tests covering fallback and exclusion cases.
Tool surcharge calculation
service/text_quota.go, service/text_quota_test.go
Quota summaries now contain sorted per-tool surcharge items and emit them in billing content and logging payloads.
Tool price configuration
setting/operation_setting/..., web/default/src/features/system-settings/models/tool-price-settings.tsx, web/default/src/i18n/locales/*
Default prices include web_extractor and code_interpreter; persisted prices merge with defaults and billing guidance is translated.
Usage-log tool breakdown
web/default/src/features/usage-logs/...
Usage logs accept per-tool call details and render individual tool rows when available, retaining legacy search rows otherwise.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OpenAIResponses
  participant OaiResponsesHandler
  participant ResponsesUsageInfo
  participant TextQuota
  participant UsageLog
  OpenAIResponses->>OaiResponsesHandler: output items and usage.tool_usage
  OaiResponsesHandler->>ResponsesUsageInfo: record provisional and final tool counts
  ResponsesUsageInfo->>TextQuota: built-in tool call counts
  TextQuota->>UsageLog: tool_calls billing payload
  UsageLog->>UsageLog: render per-tool billing rows
Loading

Possibly related PRs

Suggested reviewers: calcium-ion, seefs001, t0ng7u, feitianbubu

Poem

I’m a bunny counting calls in a row,
From search to code, the tallies grow.
Upstream stamps each tool with care,
Quota hops through billing air.
Logs now show each carrot’s name—
A tidy trail for every game.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: configurable billing for tool calls in the current request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
setting/operation_setting/tools_test.go (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use testify/assert for non-fatal value checks.

As per coding guidelines, use testify/assert for non-fatal value checks and reserve testify/require for setup and fatal assertions.

♻️ Proposed refactor
 	"testing"
 
-	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestGetToolPriceForModelSupportsConfiguredToolNames(t *testing.T) {
 	originalPrices := toolPriceSetting.Prices
 	t.Cleanup(func() {
 		toolPriceSetting.Prices = originalPrices
 		RebuildToolPriceIndex()
 	})
 
 	toolPriceSetting.Prices = map[string]float64{
 		"toutiao":                       4,
 		"toutiao:deepseek-v4-flash*":    2,
 		"web_search:deepseek-v4-flash*": 8,
 	}
 	RebuildToolPriceIndex()
 
-	require.Equal(t, 2.0, GetToolPriceForModel("toutiao", "deepseek-v4-flash"))
-	require.Equal(t, 4.0, GetToolPriceForModel("toutiao", "other-model"))
-	require.Equal(t, 8.0, GetToolPriceForModel("web_search", "deepseek-v4-flash"))
+	assert.Equal(t, 2.0, GetToolPriceForModel("toutiao", "deepseek-v4-flash"))
+	assert.Equal(t, 4.0, GetToolPriceForModel("toutiao", "other-model"))
+	assert.Equal(t, 8.0, GetToolPriceForModel("web_search", "deepseek-v4-flash"))
 }

Also applies to: 23-25

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setting/operation_setting/tools_test.go` at line 6, Replace the
testify/require usage in the tests with testify/assert for non-fatal value
checks, including the additional occurrences at the referenced assertions;
retain require only for setup failures or assertions that must stop the test
immediately.

Source: Coding guidelines

service/text_quota.go (1)

50-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

toolCallSurchargeItem duplicates ToolCallItem and forces a second formula computation downstream.

This struct re-declares the same name/call_count/price_per_1k JSON shape already defined by ToolCallItem in service/tool_billing.go (which additionally carries total_price/quota). Because the computed quota isn't stored here, the extraContent loop at Lines 377-383 has to recompute the identical price×count/1000×groupRatio×quotaPerUnit formula that addTextToolCallSurcharge (Lines 86-107) already computed — two independent implementations of the same billing math that can silently drift.

Consider either reusing ToolCallItem directly, or adding an internal (non-serialized) computed-quota field set once in addTextToolCallSurcharge and consumed by the extraContent loop.

♻️ Proposed fix sketch
 type toolCallSurchargeItem struct {
 	Name       string  `json:"name"`
 	CallCount  int     `json:"call_count"`
 	PricePer1K float64 `json:"price_per_1k"`
+	quota      decimal.Decimal `json:"-"`
 }
-	return decimal.NewFromFloat(pricePer1K).
+	quota := decimal.NewFromFloat(pricePer1K).
 		Mul(decimal.NewFromInt(int64(callCount))).
 		Div(decimal.NewFromInt(1000)).
 		Mul(decimal.NewFromFloat(summary.GroupRatio)).
 		Mul(decimal.NewFromFloat(common.QuotaPerUnit))
+	summary.ToolCallItems[len(summary.ToolCallItems)-1].quota = quota
+	return quota
-	for _, toolCall := range summary.ToolCallItems {
-		toolQuota := decimal.NewFromFloat(toolCall.PricePer1K)...
-		extraContent = append(extraContent, fmt.Sprintf(..., toolQuota.String()))
+	for _, toolCall := range summary.ToolCallItems {
+		extraContent = append(extraContent, fmt.Sprintf(..., toolCall.quota.String()))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/text_quota.go` around lines 50 - 61, Replace toolCallSurchargeItem
with the existing ToolCallItem type from tool_billing.go, and preserve the
computed quota through addTextToolCallSurcharge for the extraContent loop.
Update the downstream loop to consume that stored computed value instead of
reimplementing the price, count, group-ratio, and quota-per-unit calculation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 337-359: Update the tool-call row construction in the details
dialog so labels are localized through t() instead of rendering toolCall.name
directly. Map backend identifiers such as web_search and file_search to their
English translation keys, and use the translated result for label while
preserving existing filtering and formatting.

---

Nitpick comments:
In `@service/text_quota.go`:
- Around line 50-61: Replace toolCallSurchargeItem with the existing
ToolCallItem type from tool_billing.go, and preserve the computed quota through
addTextToolCallSurcharge for the extraContent loop. Update the downstream loop
to consume that stored computed value instead of reimplementing the price,
count, group-ratio, and quota-per-unit calculation.

In `@setting/operation_setting/tools_test.go`:
- Line 6: Replace the testify/require usage in the tests with testify/assert for
non-fatal value checks, including the additional occurrences at the referenced
assertions; retain require only for setup failures or assertions that must stop
the test immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e508f63-1dee-49a0-a583-1284ea333bca

📥 Commits

Reviewing files that changed from the base of the PR and between a63364d and 7657796.

📒 Files selected for processing (19)
  • dto/openai_response.go
  • relay/channel/openai/relay_responses.go
  • relay/channel/openai/relay_responses_test.go
  • relay/common/relay_info.go
  • relay/common/relay_info_test.go
  • service/text_quota.go
  • service/text_quota_test.go
  • setting/operation_setting/tools.go
  • setting/operation_setting/tools_test.go
  • web/default/src/features/system-settings/models/tool-price-settings.tsx
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json

Comment on lines +337 to 359
if (other.tool_calls?.length) {
for (const toolCall of other.tool_calls) {
if (!toolCall.name || toolCall.call_count <= 0) continue
rows.push({
label: toolCall.name,
value: `${toolCall.call_count}x${toolCall.price_per_1k ? ` (${fmtPrice(toolCall.price_per_1k)})` : ''}`,
})
}
} else {
if (other.web_search && other.web_search_call_count) {
rows.push({
label: t('Web Search'),
value: `${other.web_search_call_count}x${other.web_search_price ? ` (${fmtPrice(other.web_search_price)})` : ''}`,
})
}

if (other.file_search && other.file_search_call_count) {
rows.push({
label: t('File Search'),
value: `${other.file_search_call_count}x${other.file_search_price ? ` (${fmtPrice(other.file_search_price)})` : ''}`,
})
if (other.file_search && other.file_search_call_count) {
rows.push({
label: t('File Search'),
value: `${other.file_search_call_count}x${other.file_search_price ? ` (${fmtPrice(other.file_search_price)})` : ''}`,
})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tool-call labels bypass t(), breaking localization for this row.

label: toolCall.name renders the raw backend identifier (e.g. web_search, file_search, or any newly configured tool name) directly, unlike the else fallback which properly localizes via t('Web Search')/t('File Search'). Non-English users will see raw snake_case identifiers instead of translated labels for tool-call rows.

🌐 Proposed fix
       rows.push({
-        label: toolCall.name,
+        label: t(toolCall.name),
         value: `${toolCall.call_count}x${toolCall.price_per_1k ? ` (${fmtPrice(toolCall.price_per_1k)})` : ''}`,
       })

Locale JSON files would need entries keyed by each backend tool name (e.g. "web_search", "file_search", "code_interpreter") to produce readable labels.

As per path instructions: "User-facing frontend text must use useTranslation() and t('English key')".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (other.tool_calls?.length) {
for (const toolCall of other.tool_calls) {
if (!toolCall.name || toolCall.call_count <= 0) continue
rows.push({
label: toolCall.name,
value: `${toolCall.call_count}x${toolCall.price_per_1k ? ` (${fmtPrice(toolCall.price_per_1k)})` : ''}`,
})
}
} else {
if (other.web_search && other.web_search_call_count) {
rows.push({
label: t('Web Search'),
value: `${other.web_search_call_count}x${other.web_search_price ? ` (${fmtPrice(other.web_search_price)})` : ''}`,
})
}
if (other.file_search && other.file_search_call_count) {
rows.push({
label: t('File Search'),
value: `${other.file_search_call_count}x${other.file_search_price ? ` (${fmtPrice(other.file_search_price)})` : ''}`,
})
if (other.file_search && other.file_search_call_count) {
rows.push({
label: t('File Search'),
value: `${other.file_search_call_count}x${other.file_search_price ? ` (${fmtPrice(other.file_search_price)})` : ''}`,
})
}
}
if (other.tool_calls?.length) {
for (const toolCall of other.tool_calls) {
if (!toolCall.name || toolCall.call_count <= 0) continue
rows.push({
label: t(toolCall.name),
value: `${toolCall.call_count}x${toolCall.price_per_1k ? ` (${fmtPrice(toolCall.price_per_1k)})` : ''}`,
})
}
} else {
if (other.web_search && other.web_search_call_count) {
rows.push({
label: t('Web Search'),
value: `${other.web_search_call_count}x${other.web_search_price ? ` (${fmtPrice(other.web_search_price)})` : ''}`,
})
}
if (other.file_search && other.file_search_call_count) {
rows.push({
label: t('File Search'),
value: `${other.file_search_call_count}x${other.file_search_price ? ` (${fmtPrice(other.file_search_price)})` : ''}`,
})
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`
around lines 337 - 359, Update the tool-call row construction in the details
dialog so labels are localized through t() instead of rendering toolCall.name
directly. Map backend identifiers such as web_search and file_search to their
English translation keys, and use the translated result for label while
preserving existing filtering and formatting.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant