Skip to content

refactor: optimize billing flow for OpenAI-to-Anthropic convert - #3398

Closed
seefs001 wants to merge 2 commits into
QuantumNous:mainfrom
seefs001:claude2openaiusage
Closed

refactor: optimize billing flow for OpenAI-to-Anthropic convert#3398
seefs001 wants to merge 2 commits into
QuantumNous:mainfrom
seefs001:claude2openaiusage

Conversation

@seefs001

@seefs001 seefs001 commented Mar 23, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added semantic tagging to distinguish Claude and OpenAI usage in logs.
    • Implemented split cache token tracking for 5-minute and 1-hour cache windows.
    • Enhanced usage logs to display detailed cache creation metrics and improved input token reporting.
  • Bug Fixes

    • Improved quota calculation accuracy with better support for cache token accounting.
  • Tests

    • Added comprehensive test coverage for usage conversion and cache token calculations.

@coderabbitai

coderabbitai Bot commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR consolidates quota consumption logic by introducing a unified PostTextConsumeQuota service function to replace Claude-specific and local helpers. It adds semantic field tracking to Usage DTOs, restructures quota calculation and billing for text-based consumption, and updates web UI rendering for improved cache token and pricing visibility.

Changes

Cohort / File(s) Summary
DTO Additions
dto/openai_response.go, dto/openai_response_test.go
Added UsageSemantic and UsageSource fields to Usage; enabled JSON serialization of InputTokenDetails.CachedCreationTokens. Test validates Anthropic-specific field round-tripping via marshal/unmarshal.
Quota System Refactoring
service/quota.go, service/text_quota.go, service/text_quota_test.go
Removed PostClaudeConsumeQuota and introduced unified PostTextConsumeQuota with comprehensive quota summary logic, cache-token normalization, Claude semantic detection, and granular billing component tracking. Tests cover semantic handling and cache-creation token split ratios (5m/1h).
Relay Handler Updates
relay/audio_handler.go, relay/embedding_handler.go, relay/gemini_handler.go, relay/image_handler.go, relay/rerank_handler.go, relay/responses_handler.go, relay/claude_handler.go, relay/compatible_handler.go
Unified quota posting by replacing local postConsumeQuota calls and Claude-specific functions with service.PostTextConsumeQuota(..., nil) across all text-based handlers. Removed ~293 lines of local quota logic from compatible_handler.go.
Claude Relay Logic
relay/channel/claude/relay-claude.go, relay/channel/claude/relay_claude_test.go
Added OpenAI-style usage conversion helper (buildOpenAIStyleUsageFromClaudeUsage) and explicit semantic field setting (UsageSemantic tags). Updated stream/non-stream response paths to use converted usage. Test coverage for cache token aggregation and 5m/1h split scenarios.
Service Conversion & Logging
service/convert.go, service/log_info_generate.go
Centralized Claude-to-OpenAI usage conversion in buildClaudeUsageFromOpenAIUsage. Added appendFinalRequestFormat enrichment to log info generation for Claude relay format detection.
Web UI Rendering
web/src/helpers/render.jsx
Extended model price and log content rendering functions with cache-creation parameters (legacy and split 5m/1h). Updated billing wording, removed obsolete helpers, and refactored article-based rendering to inline JSX with i18n. ~70 net line addition to support granular cache token breakdown.
Web UI Components & Hooks
web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx, web/src/components/table/usage-logs/detailSummary.js, web/src/hooks/usage-logs/useUsageLogsData.jsx
Added normalizeDetailSegments utility for consistent detail summary formatting. Updated input token column rendering logic to distinguish Claude vs non-Claude formats using input_tokens_total. Introduced split cache-creation token expansion (5m/1h) in log details, and extended billing/content rendering calls with new cache-creation metrics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion
  • creamlike1024

Poem

🐰 Through the warren of quota paths, now unified and bright,
Cache creation tokens split (5m/1h) into the light,
Semantic tags mark Claude and OpenAI with care,
Billing breakdown rendered fair for all to share!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: refactoring and optimizing the billing flow specifically for OpenAI-to-Anthropic conversion, which aligns with the core changes consolidating quota consumption into a unified service layer and adding semantic usage tracking.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/claude/relay-claude.go`:
- Around line 576-583: The recomputed totalInputTokens includes
cacheCreationTokens but you never update the per-field cached_creation token
counter; set clone.PromptTokensDetails.CachedCreationTokens =
cacheCreationTokens (ensuring PromptTokensDetails is non-nil) immediately after
computing cacheCreationTokens/totalInputTokens so the OpenAI-shaped usage
(clone.PromptTokens, clone.InputTokens, clone.TotalTokens) stays consistent with
the per-field breakdown; this touches the variables/functions
cacheCreationTokensForOpenAIUsage, cacheCreationTokens, totalInputTokens, clone
and clone.PromptTokensDetails.CachedCreationTokens.

In `@service/text_quota.go`:
- Around line 106-114: summary.TotalTokens is incorrectly set to only
PromptTokens + CompletionTokens; update the calculation and related gating logic
to include all billable signals (cache creations/writes, cache read/write
distinctions you track, PromptTokensDetails.CachedCreationTokens,
CacheCreationTokens, CacheCreationTokens5m/1h, PromptTokensDetails.ImageTokens,
PromptTokensDetails.AudioTokens and any web/file/tool/fixed-price token fields
from usage) so TotalTokens reflects true billable usage; then change the later
conditional blocks referenced (the logic around resetting to 0 at the section
that currently covers lines 272-275 and the user/channel usage skip at the
section that currently covers lines 319-325) to use this new TotalTokens or an
explicit isBillable flag (e.g., hasBillableTokens := sumOfAllBillableFields > 0)
rather than the previous Prompt+Completion-only check so cache-only or tool-only
requests are handled correctly.

In `@web/src/hooks/usage-logs/useUsageLogsData.jsx`:
- Around line 384-386: The current hasSplitCacheCreation guard only checks for
cache_creation_tokens_5m/_1h and suppresses the aggregate total when the splits
don't sum to the stored total; update the logic that computes
hasSplitCacheCreation to also compute splitSum =
(other?.cache_creation_tokens_5m || 0) + (other?.cache_creation_tokens_1h || 0)
and, if other?.cache_creation_tokens exists and other.cache_creation_tokens >
splitSum, treat this as an "incomplete split" and preserve/show the aggregate
total (i.e., do not hide other.cache_creation_tokens), applying the same change
to the other occurrence around the 428-445 block so aggregate totals are kept
when splits don't add up.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b6153fa-cd43-4447-a1a9-40f2806ab5f1

📥 Commits

Reviewing files that changed from the base of the PR and between deff59a and 9b5632f.

📒 Files selected for processing (21)
  • dto/openai_response.go
  • dto/openai_response_test.go
  • relay/audio_handler.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go
  • relay/claude_handler.go
  • relay/compatible_handler.go
  • relay/embedding_handler.go
  • relay/gemini_handler.go
  • relay/image_handler.go
  • relay/rerank_handler.go
  • relay/responses_handler.go
  • service/convert.go
  • service/log_info_generate.go
  • service/quota.go
  • service/text_quota.go
  • service/text_quota_test.go
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/src/components/table/usage-logs/detailSummary.js
  • web/src/helpers/render.jsx
  • web/src/hooks/usage-logs/useUsageLogsData.jsx
💤 Files with no reviewable changes (1)
  • service/quota.go

Comment on lines +576 to +583
clone := *usage
cacheCreationTokens := cacheCreationTokensForOpenAIUsage(usage)
totalInputTokens := usage.PromptTokens + usage.PromptTokensDetails.CachedTokens + cacheCreationTokens
clone.PromptTokens = totalInputTokens
clone.InputTokens = totalInputTokens
clone.TotalTokens = totalInputTokens + usage.CompletionTokens
clone.UsageSemantic = "openai"
clone.UsageSource = "anthropic"

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.

⚠️ Potential issue | 🟠 Major

Normalize cached_creation_tokens alongside the recomputed prompt total.

totalInputTokens includes cacheCreationTokens, but clone.PromptTokensDetails.CachedCreationTokens is left unchanged. When Claude only sends the split 5m/1h fields, the OpenAI-shaped usage returned from Line 797 and Line 858 will report prompt/input totals that include cache creation while cached_creation_tokens stays 0 or disappears entirely, and any OpenAI-semantic quota math will overcount base tokens.

🩹 Proposed fix
 func buildOpenAIStyleUsageFromClaudeUsage(usage *dto.Usage) dto.Usage {
 	if usage == nil {
 		return dto.Usage{}
 	}
 	clone := *usage
 	cacheCreationTokens := cacheCreationTokensForOpenAIUsage(usage)
+	clone.PromptTokensDetails.CachedCreationTokens = cacheCreationTokens
 	totalInputTokens := usage.PromptTokens + usage.PromptTokensDetails.CachedTokens + cacheCreationTokens
 	clone.PromptTokens = totalInputTokens
 	clone.InputTokens = totalInputTokens
 	clone.TotalTokens = totalInputTokens + usage.CompletionTokens
 	clone.UsageSemantic = "openai"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay-claude.go` around lines 576 - 583, The recomputed
totalInputTokens includes cacheCreationTokens but you never update the per-field
cached_creation token counter; set
clone.PromptTokensDetails.CachedCreationTokens = cacheCreationTokens (ensuring
PromptTokensDetails is non-nil) immediately after computing
cacheCreationTokens/totalInputTokens so the OpenAI-shaped usage
(clone.PromptTokens, clone.InputTokens, clone.TotalTokens) stays consistent with
the per-field breakdown; this touches the variables/functions
cacheCreationTokensForOpenAIUsage, cacheCreationTokens, totalInputTokens, clone
and clone.PromptTokensDetails.CachedCreationTokens.

Comment thread service/text_quota.go
Comment on lines +106 to +114
summary.PromptTokens = usage.PromptTokens
summary.CompletionTokens = usage.CompletionTokens
summary.TotalTokens = usage.PromptTokens + usage.CompletionTokens
summary.CacheTokens = usage.PromptTokensDetails.CachedTokens
summary.CacheCreationTokens = usage.PromptTokensDetails.CachedCreationTokens
summary.CacheCreationTokens5m = usage.ClaudeCacheCreation5mTokens
summary.CacheCreationTokens1h = usage.ClaudeCacheCreation1hTokens
summary.ImageTokens = usage.PromptTokensDetails.ImageTokens
summary.AudioTokens = usage.PromptTokensDetails.AudioTokens

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.

⚠️ Potential issue | 🔴 Critical

Don't use PromptTokens + CompletionTokens as the only billable signal.

summary.TotalTokens ignores cache reads/writes, image/audio tokens, web/file search, and fixed-price calls. A cache-only or tool-only request can compute a positive quota above, then Lines 272-275 reset it to 0 and Lines 319-325 skip the user/channel usage counters as well.

💡 Suggested fix
+func hasBillableUsage(summary textQuotaSummary, relayInfo *relaycommon.RelayInfo) bool {
+	return summary.PromptTokens > 0 ||
+		summary.CompletionTokens > 0 ||
+		summary.CacheTokens > 0 ||
+		cacheWriteTokensTotal(summary) > 0 ||
+		summary.ImageTokens > 0 ||
+		summary.AudioTokens > 0 ||
+		summary.WebSearchCallCount > 0 ||
+		summary.ClaudeWebSearchCallCount > 0 ||
+		summary.FileSearchCallCount > 0 ||
+		summary.ImageGenerationCallPrice > 0 ||
+		(relayInfo.PriceData.UsePrice && summary.ModelPrice > 0)
+}
...
-	if summary.TotalTokens == 0 {
+	if !hasBillableUsage(summary, relayInfo) {
 		summary.Quota = 0
 	} else if !ratio.IsZero() && summary.Quota == 0 {
 		summary.Quota = 1
 	}
...
-	if summary.TotalTokens == 0 {
+	if !hasBillableUsage(summary, relayInfo) {
 		extraContent = append(extraContent, "上游没有返回计费信息,无法扣费(可能是上游超时)")
 		logger.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, summary.ModelName, relayInfo.FinalPreConsumedQuota))
 	} else {
 		model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, summary.Quota)
 		model.UpdateChannelUsedQuota(relayInfo.ChannelId, summary.Quota)
 	}

Also applies to: 272-275, 319-325

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/text_quota.go` around lines 106 - 114, summary.TotalTokens is
incorrectly set to only PromptTokens + CompletionTokens; update the calculation
and related gating logic to include all billable signals (cache
creations/writes, cache read/write distinctions you track,
PromptTokensDetails.CachedCreationTokens, CacheCreationTokens,
CacheCreationTokens5m/1h, PromptTokensDetails.ImageTokens,
PromptTokensDetails.AudioTokens and any web/file/tool/fixed-price token fields
from usage) so TotalTokens reflects true billable usage; then change the later
conditional blocks referenced (the logic around resetting to 0 at the section
that currently covers lines 272-275 and the user/channel usage skip at the
section that currently covers lines 319-325) to use this new TotalTokens or an
explicit isBillable flag (e.g., hasBillableTokens := sumOfAllBillableFields > 0)
rather than the previous Prompt+Completion-only check so cache-only or tool-only
requests are handled correctly.

Comment on lines +384 to +386
const hasSplitCacheCreation =
(other?.cache_creation_tokens_5m || 0) > 0 ||
(other?.cache_creation_tokens_1h || 0) > 0;

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.

⚠️ Potential issue | 🟠 Major

Keep the aggregate cache-creation total when split windows do not add up to it.

The backend now preserves cache_creation_tokens alongside cache_creation_tokens_5m/_1h when there is an unsplit remainder. With the new guard here, a record like 50 total / 10 (5m) / 20 (1h) only shows 30 in the expanded details, while the prompt column still shows 50 via the normalized cache-write total.

💡 Suggested fix
-      const hasSplitCacheCreation =
-        (other?.cache_creation_tokens_5m || 0) > 0 ||
-        (other?.cache_creation_tokens_1h || 0) > 0;
+      const cacheCreationTokens = Number(other?.cache_creation_tokens || 0);
+      const cacheCreationTokens5m = Number(
+        other?.cache_creation_tokens_5m || 0,
+      );
+      const cacheCreationTokens1h = Number(
+        other?.cache_creation_tokens_1h || 0,
+      );
+      const splitCacheCreationTotal =
+        cacheCreationTokens5m + cacheCreationTokens1h;
+      const hasSplitCacheCreation = splitCacheCreationTotal > 0;
...
-      if (!hasSplitCacheCreation && other?.cache_creation_tokens > 0) {
+      if (
+        cacheCreationTokens > 0 &&
+        (!hasSplitCacheCreation ||
+          cacheCreationTokens > splitCacheCreationTotal)
+      ) {
         expandDataLocal.push({
           key: t('缓存创建 Tokens'),
-          value: other.cache_creation_tokens,
+          value: cacheCreationTokens,
         });
       }
-      if (other?.cache_creation_tokens_5m > 0) {
+      if (cacheCreationTokens5m > 0) {
         expandDataLocal.push({
           key: t('缓存创建 Tokens (5m)'),
-          value: other.cache_creation_tokens_5m,
+          value: cacheCreationTokens5m,
         });
       }
-      if (other?.cache_creation_tokens_1h > 0) {
+      if (cacheCreationTokens1h > 0) {
         expandDataLocal.push({
           key: t('缓存创建 Tokens (1h)'),
-          value: other.cache_creation_tokens_1h,
+          value: cacheCreationTokens1h,
         });
       }

Also applies to: 428-445

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/hooks/usage-logs/useUsageLogsData.jsx` around lines 384 - 386, The
current hasSplitCacheCreation guard only checks for cache_creation_tokens_5m/_1h
and suppresses the aggregate total when the splits don't sum to the stored
total; update the logic that computes hasSplitCacheCreation to also compute
splitSum = (other?.cache_creation_tokens_5m || 0) +
(other?.cache_creation_tokens_1h || 0) and, if other?.cache_creation_tokens
exists and other.cache_creation_tokens > splitSum, treat this as an "incomplete
split" and preserve/show the aggregate total (i.e., do not hide
other.cache_creation_tokens), applying the same change to the other occurrence
around the 428-445 block so aggregate totals are kept when splits don't add up.

@seefs001 seefs001 closed this Mar 23, 2026
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