feat: add configurable current-request tool billing#6227
Conversation
WalkthroughOpenAI 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. ChangesTool usage billing
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
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
setting/operation_setting/tools_test.go (1)
6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse
testify/assertfor non-fatal value checks.As per coding guidelines, use
testify/assertfor non-fatal value checks and reservetestify/requirefor 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
toolCallSurchargeItemduplicatesToolCallItemand forces a second formula computation downstream.This struct re-declares the same
name/call_count/price_per_1kJSON shape already defined byToolCallIteminservice/tool_billing.go(which additionally carriestotal_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 thataddTextToolCallSurcharge(Lines 86-107) already computed — two independent implementations of the same billing math that can silently drift.Consider either reusing
ToolCallItemdirectly, or adding an internal (non-serialized) computed-quota field set once inaddTextToolCallSurchargeand 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
📒 Files selected for processing (19)
dto/openai_response.gorelay/channel/openai/relay_responses.gorelay/channel/openai/relay_responses_test.gorelay/common/relay_info.gorelay/common/relay_info_test.goservice/text_quota.goservice/text_quota_test.gosetting/operation_setting/tools.gosetting/operation_setting/tools_test.goweb/default/src/features/system-settings/models/tool-price-settings.tsxweb/default/src/features/usage-logs/components/dialogs/details-dialog.tsxweb/default/src/features/usage-logs/types.tsweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh-TW.jsonweb/default/src/i18n/locales/zh.json
| 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)})` : ''}`, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 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.
| 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
Important
📝 变更描述 / Description
针对于火山/百炼的response api请求, 实现工具调用的计费, 原平台中只能识别web_search_preview的工具调用计费.
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
Summary by CodeRabbit