feat: support Claude messages token counting#5596
Conversation
WalkthroughAdds support for the Claude ChangesClaude count_tokens endpoint support
Group-based subscription payment amount
Sequence Diagram(s)sequenceDiagram
participant Client
participant RelayController
participant ClaudeHelper
participant UpstreamClaude
Client->>RelayController: POST /v1/messages/count_tokens
RelayController->>RelayController: IsClaudeCountTokensRequest → FinalPreConsumedQuota=0, skip PreConsumeBilling
RelayController->>ClaudeHelper: ClaudeHelper(info, request)
ClaudeHelper->>ClaudeHelper: clear MaxTokens, MaxTokensToSample, Stream=nil
ClaudeHelper->>UpstreamClaude: GET /v1/messages/count_tokens (via adaptor URL)
UpstreamClaude-->>ClaudeHelper: response body
ClaudeHelper->>ClaudeHelper: IsClaudeCountTokensRequest → read + close body, skip DoResponse/quota
ClaudeHelper-->>Client: copy raw response body, return nil
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 3
🤖 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 `@controller/subscription_payment_epay.go`:
- Line 92: The int64 cast in the getPayMoney call is truncating the fractional
cents from plan.PriceAmount (a float64 representing decimal currency amounts).
Instead of casting directly to int64, multiply plan.PriceAmount by 100 to
convert dollars to cents, then cast to int64, ensuring full precision is
preserved throughout the payment calculation. Make the corresponding adjustment
to the getPayMoney function signature and implementation to handle the input as
cents rather than dollars. Apply this same fix to the identical pattern in
subscription_payment_waffo_pancake.go to ensure consistency across all payment
methods.
In `@controller/subscription_payment_waffo_pancake.go`:
- Around line 82-86: In the getWaffoPancakePayMoney function call, remove the
int64() type cast on plan.PriceAmount to prevent truncation of decimal values;
pass plan.PriceAmount directly to the function. Additionally, change the payment
amount threshold comparison from < 0.01 to <= 0.01 to align with the consistent
threshold check used in topup_waffo_pancake.go for the same payment provider.
In `@relay/channel/claude/adaptor_test.go`:
- Around line 14-16: The RelayInfo struct literal is attempting to set
ChannelBaseUrl directly, but since ChannelMeta is embedded as a pointer
(*ChannelMeta) rather than directly embedded, Go's struct literal syntax cannot
set its promoted fields this way. Fix this by explicitly initializing the
ChannelMeta field with a pointer to a new ChannelMeta struct that contains the
ChannelBaseUrl value. This applies to both locations mentioned in the comment
(around lines 14 and 26) where RelayInfo struct literals are being constructed
with ChannelBaseUrl and RelayMode fields.
🪄 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: d22d449d-3bdb-4dbb-bfd1-d825443a1944
📒 Files selected for processing (11)
controller/relay.gocontroller/subscription_payment_epay.gocontroller/subscription_payment_waffo_pancake.gorelay/channel/claude/adaptor.gorelay/channel/claude/adaptor_test.gorelay/claude_handler.gorelay/common/relay_info.gorelay/common/relay_info_test.gorelay/constant/relay_mode.gorelay/constant/relay_mode_test.gorouter/relay-router.go
| c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"}) | ||
| return | ||
| } | ||
| payMoney := getPayMoney(int64(plan.PriceAmount), group) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify plan.PriceAmount type and getPayMoney signature
echo "=== Check SubscriptionPlan model for PriceAmount type ==="
rg -n "PriceAmount" --type go -C3 | head -60
echo ""
echo "=== Check getPayMoney signature ==="
ast-grep --pattern 'func getPayMoney($$$) $_ {
$$$
}'Repository: QuantumNous/new-api
Length of output: 5059
🏁 Script executed:
#!/bin/bash
# Verify the actual line 92 in subscription_payment_epay.go
echo "=== Lines 85-105 in subscription_payment_epay.go ==="
sed -n '85,105p' controller/subscription_payment_epay.go
echo ""
echo "=== Check SubscriptionOrder Money field type ==="
rg -n "type SubscriptionOrder struct" -A 20 model/subscription.go | head -30
echo ""
echo "=== Search for cents convention patterns ==="
rg -n "int64.*100\|cents\|\* 100" --type go controller/ | head -20Repository: QuantumNous/new-api
Length of output: 1651
🏁 Script executed:
#!/bin/bash
# Check how other payment methods handle plan.PriceAmount
echo "=== Check other payment method handlers (balance, stripe, etc) ==="
rg -n "payMoney\|Money.*plan.PriceAmount\|Money.*plan\.Price" --type go controller/ -B2 -A2 | head -50
echo ""
echo "=== Check if there's a float64-based payment function ==="
rg -n "func.*Payment.*float64\|func.*Money.*float64" --type go controller/ | grep -v test
echo ""
echo "=== Look at balance payment implementation for comparison ==="
rg -n "PaymentMethodBalance\|PaymentProviderBalance" --type go -A10 controller/subscription*.go | head -60Repository: QuantumNous/new-api
Length of output: 252
🏁 Script executed:
#!/bin/bash
# Check the full subscription payment file for other methods
echo "=== Check subscription_payment files ==="
ls -la controller/subscription_payment*.go
echo ""
echo "=== Check balance payment method in detail ==="
rg -n "handleSubscriptionBalancePay\|PaymentMethodBalance" --type go -B5 -A15 model/subscription.go | head -80
echo ""
echo "=== Check how getPayMoney is used elsewhere ==="
rg -n "getPayMoney" --type go -B2 -A2Repository: QuantumNous/new-api
Length of output: 1689
🏁 Script executed:
#!/bin/bash
# Check other subscription payment methods for how they handle plan.PriceAmount
echo "=== Check subscription_payment_stripe.go ==="
sed -n '1,120p' controller/subscription_payment_stripe.go | grep -A5 -B5 "plan.PriceAmount\|Money.*plan"
echo ""
echo "=== Check subscription_payment_creem.go ==="
sed -n '1,120p' controller/subscription_payment_creem.go | grep -A5 -B5 "plan.PriceAmount\|Money.*plan"
echo ""
echo "=== Check subscription_payment_waffo_pancake.go ==="
sed -n '1,120p' controller/subscription_payment_waffo_pancake.go | grep -A5 -B5 "plan.PriceAmount\|Money.*plan"Repository: QuantumNous/new-api
Length of output: 1532
🏁 Script executed:
#!/bin/bash
# Check getWaffoPancakePayMoney function
echo "=== Check getWaffoPancakePayMoney function ==="
rg -n "func getWaffoPancakePayMoney" --type go -A20
echo ""
echo "=== Check complete epay payment file to understand context ==="
cat -n controller/subscription_payment_epay.go | sed -n '1,120p'Repository: QuantumNous/new-api
Length of output: 5439
🏁 Script executed:
#!/bin/bash
# Final verification: Check if topup Amount is in cents or dollars
echo "=== Check TopupRequest structure ==="
rg -n "type TopupRequest\|type.*Request struct" --type go controller/topup.go -A10 | head -40
echo ""
echo "=== Check how req.Amount is used in topup vs subscription ==="
echo "In topup.go context:"
rg -n "req.Amount" controller/topup.go | head -10
echo ""
echo "=== Verify if there's a cents convention in topup ==="
rg -n "Amount.*100\|cents" --type go controller/topup.go | head -10Repository: QuantumNous/new-api
Length of output: 1132
int64() truncation loses fractional price amounts—affects billing accuracy
plan.PriceAmount is float64 (stored as decimal(10,6) in the database), but casting to int64 truncates decimals. A plan priced at $9.99 becomes $9, undercharging by $0.99.
The issue appears in both subscription payment methods:
subscription_payment_epay.goline 92subscription_payment_waffo_pancake.go(similar pattern)
Other payment methods (Stripe, Creem) avoid this by using plan.PriceAmount directly without conversion.
Consider either:
- Passing cents:
int64(plan.PriceAmount * 100)and adjustinggetPayMoneyto work in cents, or - Creating a float-aware helper for subscription pricing that preserves precision (similar to how Stripe/Creem handle it).
🤖 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 `@controller/subscription_payment_epay.go` at line 92, The int64 cast in the
getPayMoney call is truncating the fractional cents from plan.PriceAmount (a
float64 representing decimal currency amounts). Instead of casting directly to
int64, multiply plan.PriceAmount by 100 to convert dollars to cents, then cast
to int64, ensuring full precision is preserved throughout the payment
calculation. Make the corresponding adjustment to the getPayMoney function
signature and implementation to handle the input as cents rather than dollars.
Apply this same fix to the identical pattern in
subscription_payment_waffo_pancake.go to ensure consistency across all payment
methods.
| payMoney := getWaffoPancakePayMoney(int64(plan.PriceAmount), user.Group) | ||
| if payMoney < 0.01 { | ||
| c.JSON(http.StatusOK, gin.H{"message": "error", "data": "套餐金额过低"}) | ||
| return | ||
| } |
There was a problem hiding this comment.
Same int64() truncation issue as epay handler
int64(plan.PriceAmount) truncates decimal prices (see comment on subscription_payment_epay.go line 92).
Additionally, the comparison < 0.01 differs from the existing topup_waffo_pancake.go which uses <= 0.01 (context snippet 2, line 45). For consistency within the same payment provider, consider aligning the threshold check.
🤖 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 `@controller/subscription_payment_waffo_pancake.go` around lines 82 - 86, In
the getWaffoPancakePayMoney function call, remove the int64() type cast on
plan.PriceAmount to prevent truncation of decimal values; pass plan.PriceAmount
directly to the function. Additionally, change the payment amount threshold
comparison from < 0.01 to <= 0.01 to align with the consistent threshold check
used in topup_waffo_pancake.go for the same payment provider.
| ChannelBaseUrl: "https://api.anthropic.com", | ||
| RelayMode: relayconstant.RelayModeClaudeCountTokens, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== RelayInfo and ChannelMeta declarations =="
rg -n --type=go 'type RelayInfo struct|type ChannelMeta struct|ChannelBaseUrl' relay/common/relay_info.go relay/common/channel_meta.go || true
echo
echo "== Invalid test literal usage =="
rg -nPU --type=go 'RelayInfo\s*{(?s).*?ChannelBaseUrl\s*:' relay/channel/claude/adaptor_test.goRepository: QuantumNous/new-api
Length of output: 952
🏁 Script executed:
# View the RelayInfo and ChannelMeta struct definitions
sed -n '64,115p' relay/common/relay_info.goRepository: QuantumNous/new-api
Length of output: 1580
🏁 Script executed:
# View the full test context around the problematic lines
sed -n '10,35p' relay/channel/claude/adaptor_test.goRepository: QuantumNous/new-api
Length of output: 818
🏁 Script executed:
sed -n '89,120p' relay/common/relay_info.goRepository: QuantumNous/new-api
Length of output: 1084
🏁 Script executed:
sed -n '120,150p' relay/common/relay_info.goRepository: QuantumNous/new-api
Length of output: 1502
🏁 Script executed:
sed -n '150,180p' relay/common/relay_info.goRepository: QuantumNous/new-api
Length of output: 1502
🏁 Script executed:
sed -n '180,210p' relay/common/relay_info.goRepository: QuantumNous/new-api
Length of output: 1474
Fix invalid RelayInfo struct literals (compile blocker).
Lines 14 and 26 attempt to initialize ChannelBaseUrl directly on RelayInfo, but ChannelMeta is embedded as a pointer (*ChannelMeta), not a direct field. Struct literal syntax cannot set promoted fields of pointer-embedded types. Initialize ChannelMeta explicitly instead.
Proposed fix
func TestGetRequestURLUsesMessagesCountTokensPath(t *testing.T) {
adaptor := &Adaptor{}
info := &relaycommon.RelayInfo{
- ChannelBaseUrl: "https://api.anthropic.com",
- RelayMode: relayconstant.RelayModeClaudeCountTokens,
+ ChannelMeta: &relaycommon.ChannelMeta{
+ ChannelBaseUrl: "https://api.anthropic.com",
+ },
+ RelayMode: relayconstant.RelayModeClaudeCountTokens,
}
@@
func TestGetRequestURLKeepsMessagesPathForClaudeMessages(t *testing.T) {
adaptor := &Adaptor{}
info := &relaycommon.RelayInfo{
- ChannelBaseUrl: "https://api.anthropic.com",
- RequestURLPath: "/v1/messages",
+ ChannelMeta: &relaycommon.ChannelMeta{
+ ChannelBaseUrl: "https://api.anthropic.com",
+ },
+ RequestURLPath: "/v1/messages",
}🧰 Tools
🪛 golangci-lint (2.12.2)
[error] 14-14: : # github.com/QuantumNous/new-api/relay/channel/claude [github.com/QuantumNous/new-api/relay/channel/claude.test]
relay/channel/claude/adaptor_test.go:14:3: unknown field ChannelBaseUrl in struct literal of type "github.com/QuantumNous/new-api/relay/common".RelayInfo
relay/channel/claude/adaptor_test.go:26:3: unknown field ChannelBaseUrl in struct literal of type "github.com/QuantumNous/new-api/relay/common".RelayInfo
(typecheck)
🤖 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 `@relay/channel/claude/adaptor_test.go` around lines 14 - 16, The RelayInfo
struct literal is attempting to set ChannelBaseUrl directly, but since
ChannelMeta is embedded as a pointer (*ChannelMeta) rather than directly
embedded, Go's struct literal syntax cannot set its promoted fields this way.
Fix this by explicitly initializing the ChannelMeta field with a pointer to a
new ChannelMeta struct that contains the ChannelBaseUrl value. This applies to
both locations mentioned in the comment (around lines 14 and 26) where RelayInfo
struct literals are being constructed with ChannelBaseUrl and RelayMode fields.
Source: Linters/SAST tools
Important
📝 变更描述 / Description
支持 Anthropic Claude 官方
POST /v1/messages/count_tokens请求。新增路由会进入 Claude relay,按原始请求路径识别 count_tokens,转发到上游/v1/messages/count_tokens,成功响应直接透传input_tokens等官方返回字段,不再走普通 Messages 响应解析和文本生成扣费结算。同时补充 relay mode、请求路径识别和 Claude adaptor URL 的单元测试,确保普通
/v1/messages路径保持不变。🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
git diff --cached --checkpassed before commit.git diff --checkpassed after edits.go test/gofmtcould not be run because this environment does not havegoorgofmton PATH.POST /v1/messages/count_tokens, which returns token count data such asinput_tokens.Summary by CodeRabbit