Skip to content

feat: claude count token - #2384

Closed
seefs001 wants to merge 4770 commits into
QuantumNous:mainfrom
seefs001:feature/count_token
Closed

feat: claude count token#2384
seefs001 wants to merge 4770 commits into
QuantumNous:mainfrom
seefs001:feature/count_token

Conversation

@seefs001

@seefs001 seefs001 commented Dec 7, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added a Claude token-counting mode and a dedicated endpoint to return token estimates.
  • Behavior Changes

    • Token-counting requests skip quota pre-consume, pricing, token estimation, and channel auto-ban side effects; they receive streamlined, non-streaming responses.
  • Bug Fixes

    • Improved consistency of error handling, cleanup, and routing between standard Claude and token-counting flows.

✏️ Tip: You can customize this high-level summary in your review settings.

seefs001 and others added 30 commits October 18, 2025 13:02
fix: handle JSON parsing for thinking content in ollama stream
豆包语音2.0音色支持情感,情绪,音量
Comment out the debug log for MiniMax TTS Request.
feat: openai tts support streaming realtime audio
feat: doubao tts support streaming realtime audio
multipart/form-data; boundary
修复豆包图像编辑(图生图)功能
Calcium-Ion and others added 17 commits December 1, 2025 17:54
feat(gemini): implement markdown image handling in text processing
chore: update the relay openapi file
- Introduced new OpenAI text models in `common/model.go`.
- Added `IsOpenAITextModel` function to check for OpenAI text models.
- Refactored token estimation methods across various channels to use estimated prompt tokens instead of direct prompt token counts.
- Updated related functions and structures to accommodate the new token estimation approach, enhancing overall token management.
fix: try resolve the high concurrency issue to a single host
feat: gemini 3 thinking level gemini-3-pro-preview-high
@coderabbitai

coderabbitai Bot commented Dec 7, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a Claude "count tokens" relay mode: new RelayFormat and HTTP route, validation and relay-info wiring, adaptor support for /v1/messages/count_tokens, controller changes to bypass quota/pricing and disable channel autoban for the count-tokens path while preserving error/reporting behavior.

Changes

Cohort / File(s) Summary
Controller: relay flow
controller/relay.go
Adds ClaudeCountTokens branch: disables early channel side-effects (auto_ban off, skip_channel_autoban), skips token estimation and pricing/quotas for count-only requests, defers quota-return only on downstream error, forces channel AutoBan=0 when skip requested, and routes ClaudeCountTokens to the Claude helper.
Claude adaptor
relay/channel/claude/adaptor.go
Adds RequestModeCountTokens constant; Init maps RelayFormatClaudeCountTokens → CountTokens; GetRequestURL returns /v1/messages/count_tokens for CountTokens; DoResponse surfaces CountTokens non-streaming response body via service helpers (bypasses streaming path).
Claude handler
relay/claude_handler.go
Adds early-return guard in ClaudeHelper for RelayFormatClaudeCountTokens to skip quota consumption and post-response processing for token-count requests.
Relay info & validation
relay/common/relay_info.go, relay/helper/valid_request.go
Adds GenRelayInfoClaudeCountTokens and shared base constructor; GenRelayInfo dispatches new format; validation delegates ClaudeCountTokens to existing Claude validation.
HTTP router
router/relay-router.go
Registers POST /messages/count_tokens routing to controller.Relay with Type ClaudeCountTokens.
Types
types/relay_format.go
Adds RelayFormatClaudeCountTokens = "claude_count_tokens".
Middleware: distributor
middleware/distributor.go
Skips model-name validation for requests whose path starts with /v1/messages/count_tokens when the model is missing, allowing count_tokens requests without model.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant HTTP_Router as Router
    participant Controller
    participant Distributor
    participant Channel_Adaptor as Adaptor
    participant Claude_Service as Claude
    participant Quota_System as Quota

    Client->>Router: POST /v1/messages/count_tokens
    Router->>Controller: Relay(request, RelayFormatClaudeCountTokens)
    Controller->>Distributor: validate/select channel (path = count_tokens)
    Distributor-->>Controller: allow missing model (skip validation)
    Controller->>Adaptor: select channel (skip_channel_autoban=true)
    Adaptor-->>Controller: Channel (AutoBan=0)
    Controller->>Claude: POST /v1/messages/count_tokens (non-stream)
    Claude-->>Controller: token-count response
    Controller--xQuota: skip pre-consume (no quota consumed)
    Controller->>Client: return token-count response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Attention points:
    • Ensure pricing/quota bypass is strictly limited to ClaudeCountTokens paths in controller/relay.go
    • Verify DoResponse reads body and closes resources correctly for non-streaming CountTokens responses
    • Confirm skipAutoBan flag propagation results in Channel.AutoBan = 0 everywhere relevant
    • Check early exit in ClaudeHelper doesn't omit necessary error/report logging

Possibly related PRs

Suggested reviewers

  • Calcium-Ion

Poem

🐰📜 I hopped the route to count each token,
No quota eaten, no alarms were woken.
I nudged the channel's auto-ban to sleep,
Returned the numbers tidy and neat —
A little hop, a quiet token token!

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 'feat: claude count token' is partially related to the changeset—it mentions Claude and token counting, but it lacks clarity and conciseness. The phrase 'count token' is grammatically incomplete (should be 'count tokens') and doesn't fully capture the scope of changes, which involve broader integration of ClaudeCountTokens support across multiple components including routing, validation, channel selection, and pricing logic.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 0

🧹 Nitpick comments (1)
relay/channel/claude/adaptor.go (1)

142-142: Unreachable code: return statement after complete if-else branches.

The return on line 142 is unreachable since both the if (line 137-138) and else (line 139-141) branches return values.

 	} else {
 		return ClaudeHandler(c, resp, info, a.RequestMode)
 	}
-	return
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b9f6a5 and aec0da1.

📒 Files selected for processing (7)
  • controller/relay.go (7 hunks)
  • relay/channel/claude/adaptor.go (4 hunks)
  • relay/claude_handler.go (1 hunks)
  • relay/common/relay_info.go (1 hunks)
  • relay/helper/valid_request.go (1 hunks)
  • router/relay-router.go (1 hunks)
  • types/relay_format.go (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
📚 Learning: 2025-06-21T03:37:41.726Z
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.

Applied to files:

  • controller/relay.go
📚 Learning: 2025-08-05T17:14:17.246Z
Learnt from: neotf
Repo: QuantumNous/new-api PR: 1511
File: setting/ratio_setting/model_ratio.go:118-123
Timestamp: 2025-08-05T17:14:17.246Z
Learning: Claude models handle "-thinking" variants differently from Gemini models. For Claude models, only the base model (without "-thinking") gets an entry in defaultModelRatio map. The "-thinking" variants rely on the Claude relay handler stripping the suffix using strings.TrimSuffix(textRequest.Model, "-thinking") before looking up the ratio, so they automatically use the base model's ratio.

Applied to files:

  • controller/relay.go
  • relay/claude_handler.go
🧬 Code graph analysis (5)
relay/helper/valid_request.go (1)
types/relay_format.go (1)
  • RelayFormatClaudeCountTokens (8-8)
relay/common/relay_info.go (1)
types/relay_format.go (1)
  • RelayFormatClaudeCountTokens (8-8)
relay/channel/claude/adaptor.go (3)
types/relay_format.go (2)
  • RelayFormat (3-3)
  • RelayFormatClaudeCountTokens (8-8)
service/http.go (2)
  • CloseResponseBodyGracefully (15-23)
  • IOCopyBytesGracefully (25-60)
types/error.go (1)
  • ErrorCodeBadResponseBody (72-72)
router/relay-router.go (2)
controller/relay.go (1)
  • Relay (64-211)
types/relay_format.go (1)
  • RelayFormatClaudeCountTokens (8-8)
relay/claude_handler.go (1)
types/relay_format.go (2)
  • RelayFormat (3-3)
  • RelayFormatClaudeCountTokens (8-8)
🔇 Additional comments (15)
relay/claude_handler.go (1)

164-166: LGTM! Early return correctly bypasses quota consumption for count_tokens.

The implementation correctly allows DoResponse to handle the response (which in the adaptor copies the response body to the client) before returning early to skip PostClaudeConsumeQuota. This is the expected behavior for a token counting endpoint that shouldn't consume quota.

router/relay-router.go (1)

80-82: LGTM! Route correctly placed before the more general /messages endpoint.

The route ordering ensures /messages/count_tokens is matched before the catch-all /messages route. The route correctly delegates to controller.Relay with the ClaudeCountTokens format.

types/relay_format.go (1)

6-15: LGTM! New constant follows existing conventions.

The RelayFormatClaudeCountTokens constant is properly defined and follows the established naming pattern. The explicit type annotation on the first constant is idiomatic Go.

relay/channel/claude/adaptor.go (3)

128-136: LGTM! CountTokens response handling correctly streams raw response to client.

The implementation properly reads the upstream response body and copies it to the client without processing, which is appropriate for a token count response. The graceful close and copy helpers ensure proper resource cleanup.


50-53: LGTM! Early return correctly prevents model-based mode selection for CountTokens.

The guard clause ensures RequestModeCountTokens is set before the model prefix checks, preventing incorrect mode assignment.


63-70: LGTM! URL routing correctly maps CountTokens to the appropriate endpoint.

The switch statement cleanly routes each mode to its corresponding API path.

controller/relay.go (7)

69-69: LGTM! Clear boolean flag improves readability.

Using isClaudeCountTokens as a named flag makes the subsequent conditional checks more readable than repeating the format comparison.


118-122: LGTM! Disabling auto-ban for count_tokens prevents channel penalties for utility requests.

This correctly prevents channels from being penalized or banned due to failures on the count_tokens endpoint, which is a utility operation that shouldn't affect channel reliability metrics.


146-170: LGTM! Quota and pricing logic correctly bypassed for count_tokens.

The entire pre-consume and refund logic is wrapped in the !isClaudeCountTokens check, ensuring token counting requests don't affect user quotas. The defer block for quota refund is also correctly scoped within this conditional.


187-188: LGTM! ClaudeCountTokens correctly routes to ClaudeHelper.

Both RelayFormatClaude and RelayFormatClaudeCountTokens are handled by ClaudeHelper, which internally differentiates behavior based on the format.


227-258: LGTM! Channel selection respects skip_channel_autoban flag.

The getChannel function correctly handles the skip_channel_autoban flag for both initial channel selection (lines 234-236) and retry scenarios (lines 255-258), ensuring channels aren't banned due to count_tokens operations.


93-93: LGTM! Error response format correctly includes ClaudeCountTokens.

Both RelayFormatClaude and RelayFormatClaudeCountTokens return Claude-style error responses, maintaining API consistency.


135-142: Verify token estimation is intentionally skipped for count_tokens.

Setting tokens = 0 and skipping EstimateRequestToken means relayInfo.SetEstimatePromptTokens(0) will be called. Confirm this is intentional and identify which downstream logic depends on estimated tokens being non-zero for this code path.

relay/common/relay_info.go (1)

451-452: Potential bug: RelayFormat will be overwritten to RelayFormatClaude.

GenRelayInfoClaude sets info.RelayFormat = types.RelayFormatClaude on line 288, which will overwrite the ClaudeCountTokens format. This could cause issues downstream where the code checks for RelayFormatClaudeCountTokens.

Consider either:

  1. Setting info.RelayFormat = relayFormat after calling GenRelayInfoClaude, or
  2. Creating a dedicated GenRelayInfoClaudeCountTokens function, or
  3. Passing the format as a parameter to GenRelayInfoClaude.
relay/helper/valid_request.go (1)

35-36: The count_tokens validation is correct.

GetAndValidateClaudeRequest appropriately validates the required Messages and Model fields—both are mandatory for Claude's /v1/messages/count_tokens API endpoint.

@seefs001
seefs001 marked this pull request as draft December 9, 2025 06:40

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

🧹 Nitpick comments (1)
relay/common/relay_info.go (1)

286-305: Shared Claude base helper looks correct; consider documenting its dual use

Refactoring GenRelayInfoClaude through genRelayInfoClaudeBase keeps the previous behavior (same flags, same beta handling) while cleanly enabling GenRelayInfoClaudeCountTokens via a different RelayFormat, which is good.

Since this helper now underpins both /v1/messages and /v1/messages/count_tokens flows, consider a brief comment to make that contract explicit so future changes (e.g., pricing/usage behavior) don’t accidentally diverge:

-func genRelayInfoClaudeBase(c *gin.Context, request dto.Request, relayFormat types.RelayFormat) *RelayInfo {
+// genRelayInfoClaudeBase sets up common RelayInfo for Claude-style requests.
+// It is used by both /v1/messages and /v1/messages/count_tokens paths.
+func genRelayInfoClaudeBase(c *gin.Context, request dto.Request, relayFormat types.RelayFormat) *RelayInfo {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aec0da1 and d5dcd6b.

📒 Files selected for processing (1)
  • relay/common/relay_info.go (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
🧬 Code graph analysis (1)
relay/common/relay_info.go (2)
types/relay_format.go (3)
  • RelayFormatClaude (7-7)
  • RelayFormatClaudeCountTokens (8-8)
  • RelayFormat (3-3)
dto/request_common.go (1)
  • Request (8-12)
🔇 Additional comments (1)
relay/common/relay_info.go (1)

459-460: GenRelayInfo wiring for ClaudeCountTokens is consistent

The new RelayFormatClaudeCountTokens branch cleanly delegates to GenRelayInfoClaudeCountTokens and matches the pattern of other simple formats; no issues noted here.

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5dcd6b and f0a2fbc.

📒 Files selected for processing (1)
  • middleware/distributor.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
🧬 Code graph analysis (1)
middleware/distributor.go (2)
dto/request_common.go (1)
  • Request (8-12)
model/model_meta.go (1)
  • Model (23-44)
🔇 Additional comments (1)
middleware/distributor.go (1)

79-82: Model validation bypass is correctly implemented.

The conditional logic properly skips model name validation for count_tokens requests while preserving it for all other endpoints. The change aligns with the feature requirements described in the AI summary.

Comment thread middleware/distributor.go
Comment on lines +76 to +77
// count_tokens 接口跳过模型名称校验
isCountTokens := strings.HasPrefix(c.Request.URL.Path, "/v1/messages/count_tokens")

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

🧩 Analysis chain

🏁 Script executed:

# Search for CacheGetRandomSatisfiedChannel implementation and its handling of model parameter
rg -n "CacheGetRandomSatisfiedChannel" --type=go -B2 -A10 | head -100

Repository: QuantumNous/new-api

Length of output: 3835


🏁 Script executed:

# Check if count_tokens is handled elsewhere with shouldSelectChannel logic
rg -n "count_tokens" --type=go -B3 -A3

Repository: QuantumNous/new-api

Length of output: 3266


🏁 Script executed:

# Look for how shouldSelectChannel is set to false in the file
rg -n "shouldSelectChannel\s*=" middleware/distributor.go -B2 -A2

Repository: QuantumNous/new-api

Length of output: 1093


🏁 Script executed:

# Check the full context around line 102 where CacheGetRandomSatisfiedChannel is called
sed -n '95,110p' middleware/distributor.go

Repository: QuantumNous/new-api

Length of output: 705


🏁 Script executed:

# Get full implementation of CacheGetRandomSatisfiedChannel
sed -n '14,80p' service/channel_select.go

Repository: QuantumNous/new-api

Length of output: 1018


🏁 Script executed:

# Check what happens with modelName inside the function - look for where it's used
rg -A 50 "func CacheGetRandomSatisfiedChannel" service/channel_select.go | head -70

Repository: QuantumNous/new-api

Length of output: 1018


🏁 Script executed:

# Search for GetRandomSatisfiedChannel implementation in model package
rg -n "func GetRandomSatisfiedChannel" --type=go -A 30

Repository: QuantumNous/new-api

Length of output: 1832


🏁 Script executed:

# Also check if there's any special handling for count_tokens in controller/relay.go
sed -n '110,130p' controller/relay.go

Repository: QuantumNous/new-api

Length of output: 751


🏁 Script executed:

# Check the full context around line 102-125 to see how nil channel is handled
sed -n '95,125p' middleware/distributor.go

Repository: QuantumNous/new-api

Length of output: 1322


🏁 Script executed:

# Check if count_tokens requests even need a valid channel selected
rg -n "isCountTokens\|RelayFormatClaudeCountTokens" controller/relay.go -B5 -A10 | head -80

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Double-check by searching for how count_tokens is routed - does it even reach distributor middleware?
rg -n "count_tokens" router/relay-router.go -B3 -A5

Repository: QuantumNous/new-api

Length of output: 397


🏁 Script executed:

# Check middleware chain to understand if count_tokens goes through Distribute() middleware
sed -n '70,90p' router/relay-router.go

Repository: QuantumNous/new-api

Length of output: 680


Set shouldSelectChannel = false for count_tokens requests to avoid unnecessary channel selection failures.

The current logic skips model validation when isCountTokens is true (line 79), but then requires a valid channel at line 114, causing count_tokens requests with empty models to fail with a 503 "no available channel" error. Since count_tokens should bypass normal channel selection like other similar operations (Midjourney fetch, Suno fetch, Video fetch), add shouldSelectChannel = false when isCountTokens is true, similar to the pattern used at lines 153, 169, 179, etc.

🤖 Prompt for AI Agents
In middleware/distributor.go around lines 76-77, the isCountTokens flag is set
but the code still proceeds to channel selection causing 503 errors when model
is empty; update the logic so that when isCountTokens is true you also set
shouldSelectChannel = false (same pattern used for other bypass paths like
Midjourney/Suno/Video fetch) so count_tokens requests skip channel selection and
won't fail due to missing channel.

daymade added a commit to daymade/new-api that referenced this pull request Apr 24, 2026
Implements Anthropic's POST /v1/messages/count_tokens endpoint
(https://docs.claude.com/en/api/messages-count-tokens) by estimating
input_tokens locally instead of forwarding to any upstream channel.

Why
---
The Anthropic JS SDK and Claude CLI poll this endpoint before each
chat to size the context window. When new-api returns 404 (which it
does today, since the route is unregistered), the SDK falls back to
sending fake max_tokens=1 messages with the entire tool schema
attached. We observed bursts of 250 RPM of these probes from a single
Claude Code Desktop session, exhausting the upstream provider's RPM
quota and starving real traffic on the same channel for ~2 minutes.

Returning a fast 200 here from new-api itself fixes the failure mode
at the source: the SDK is satisfied, no upstream RPM is consumed,
no billing entry is created.

Closes QuantumNous#1694
Closes QuantumNous#2847
Closes QuantumNous#1979

Approach
--------
- Mounted on relayV1Router directly, NOT under httpRouter — the
  former gives us TokenAuth + RouteTag + SystemPerformanceCheck +
  ModelRequestRateLimit; the latter would also pull in
  middleware.Distribute() which selects a channel and starts the
  PreConsume flow. Anthropic defines count_tokens as token-counting
  only with no quota impact, so the bypass is intentional.

- Estimation reuses the project's existing pieces:
  * ClaudeRequest.GetTokenCountMeta() for the canonical flattening
    of system / messages (text, tool_use, tool_result) / tools
  * EstimateTokenByModel() for the Claude-tuned tokenizer that
    /v1/messages itself bills against
  This keeps the count consistent with what /v1/messages would
  report on the same body, so callers can reason about both numbers
  together.

- One small helper, normalizeRequestTools(), converts raw
  map[string]any tool entries (what json.Unmarshal produces when the
  field is `any`) into the typed *dto.Tool / *dto.ClaudeWebSearchTool
  values that dto.ProcessTools requires. Without this every tool
  entry on a count_tokens request would be silently dropped on the
  `default: continue` arm of ProcessTools (dto/claude.go:439-442) and
  the tools text — usually 80%+ of a CLI probe body — would not be
  counted.

- Body parsing uses common.UnmarshalBodyReusable so the request body
  remains available to any logging/observability that runs after
  this handler.

- Image tokens are intentionally not added: getImageToken() needs a
  RelayInfo + http.Request context, which this route doesn't have.
  The CLI probe (the failure mode this PR exists to mitigate) never
  carries images. Documented in the estimator's godoc.

How vs. PR QuantumNous#2384
----------------
QuantumNous#2384 took the deeper path: a new RelayFormat enum value + adaptor
changes + middleware.distributor changes + autoban bypass. That PR
was closed without merge. This PR keeps the surface area small —
one new route, one controller, one estimator, no enum/adaptor
changes, no impact on any code path that doesn't hit the new route.

Files
-----
- router/relay-router.go              + 9 lines  (one route registration)
- controller/claude_count_tokens.go   new       (~35 lines incl. doc)
- service/claude_token_estimator.go   new       (~90 lines)
- service/claude_token_estimator_test.go  new   (12 cases)

Tests
-----
$ go test ./service/ -run 'TestEstimateClaude|TestNormalizeRequestTools' -v
=== RUN   TestEstimateClaudeInputTokens
--- PASS  (10 cases: empty / short user / system+user / CLI probe /
          system as array / CJK / tool_use / tool_result / web-search /
          pre-typed tools)
=== RUN   TestEstimateClaudeInputTokens_NilSafety
--- PASS  (2 cases: nil request / zero-value request)
=== RUN   TestNormalizeRequestTools
--- PASS  (3 cases: nil / non-array / malformed dropped)

Manual verification of the failure mode (sample CLI probe body —
trivial message + Bash tool schema, ~200 bytes):
  EstimateClaudeInputTokens = 43 input tokens
  Endpoint returns: 200 OK {"input_tokens": 43}
  Claude CLI accepts and stops sending fake max_tokens=1 fallback.
daymade added a commit to daymade/new-api that referenced this pull request May 12, 2026
Implements Anthropic's POST /v1/messages/count_tokens endpoint
(https://docs.claude.com/en/api/messages-count-tokens) by estimating
input_tokens locally instead of forwarding to any upstream channel.

Why
---
The Anthropic JS SDK and Claude CLI poll this endpoint before each
chat to size the context window. When new-api returns 404 (which it
does today, since the route is unregistered), the SDK falls back to
sending fake max_tokens=1 messages with the entire tool schema
attached. We observed bursts of 250 RPM of these probes from a single
Claude Code Desktop session, exhausting the upstream provider's RPM
quota and starving real traffic on the same channel for ~2 minutes.

Returning a fast 200 here from new-api itself fixes the failure mode
at the source: the SDK is satisfied, no upstream RPM is consumed,
no billing entry is created.

Closes QuantumNous#1694
Closes QuantumNous#2847
Closes QuantumNous#1979

Approach
--------
- Mounted on relayV1Router directly, NOT under httpRouter — the
  former gives us TokenAuth + RouteTag + SystemPerformanceCheck +
  ModelRequestRateLimit; the latter would also pull in
  middleware.Distribute() which selects a channel and starts the
  PreConsume flow. Anthropic defines count_tokens as token-counting
  only with no quota impact, so the bypass is intentional.

- Estimation reuses the project's existing pieces:
  * ClaudeRequest.GetTokenCountMeta() for the canonical flattening
    of system / messages (text, tool_use, tool_result) / tools
  * EstimateTokenByModel() for the Claude-tuned tokenizer that
    /v1/messages itself bills against
  This keeps the count consistent with what /v1/messages would
  report on the same body, so callers can reason about both numbers
  together.

- One small helper, normalizeRequestTools(), converts raw
  map[string]any tool entries (what json.Unmarshal produces when the
  field is `any`) into the typed *dto.Tool / *dto.ClaudeWebSearchTool
  values that dto.ProcessTools requires. Without this every tool
  entry on a count_tokens request would be silently dropped on the
  `default: continue` arm of ProcessTools (dto/claude.go:439-442) and
  the tools text — usually 80%+ of a CLI probe body — would not be
  counted.

- Body parsing uses common.UnmarshalBodyReusable so the request body
  remains available to any logging/observability that runs after
  this handler.

- Image tokens are intentionally not added: getImageToken() needs a
  RelayInfo + http.Request context, which this route doesn't have.
  The CLI probe (the failure mode this PR exists to mitigate) never
  carries images. Documented in the estimator's godoc.

How vs. PR QuantumNous#2384
----------------
QuantumNous#2384 took the deeper path: a new RelayFormat enum value + adaptor
changes + middleware.distributor changes + autoban bypass. That PR
was closed without merge. This PR keeps the surface area small —
one new route, one controller, one estimator, no enum/adaptor
changes, no impact on any code path that doesn't hit the new route.

Files
-----
- router/relay-router.go              + 9 lines  (one route registration)
- controller/claude_count_tokens.go   new       (~35 lines incl. doc)
- service/claude_token_estimator.go   new       (~90 lines)
- service/claude_token_estimator_test.go  new   (12 cases)

Tests
-----
$ go test ./service/ -run 'TestEstimateClaude|TestNormalizeRequestTools' -v
=== RUN   TestEstimateClaudeInputTokens
--- PASS  (10 cases: empty / short user / system+user / CLI probe /
          system as array / CJK / tool_use / tool_result / web-search /
          pre-typed tools)
=== RUN   TestEstimateClaudeInputTokens_NilSafety
--- PASS  (2 cases: nil request / zero-value request)
=== RUN   TestNormalizeRequestTools
--- PASS  (3 cases: nil / non-array / malformed dropped)

Manual verification of the failure mode (sample CLI probe body —
trivial message + Bash tool schema, ~200 bytes):
  EstimateClaudeInputTokens = 43 input tokens
  Endpoint returns: 200 OK {"input_tokens": 43}
  Claude CLI accepts and stops sending fake max_tokens=1 fallback.
daymade added a commit to daymade/new-api that referenced this pull request Jul 18, 2026
Implements Anthropic's POST /v1/messages/count_tokens endpoint
(https://docs.claude.com/en/api/messages-count-tokens) by estimating
input_tokens locally instead of forwarding to any upstream channel.

Why
---
The Anthropic JS SDK and Claude CLI poll this endpoint before each
chat to size the context window. When new-api returns 404 (which it
does today, since the route is unregistered), the SDK falls back to
sending fake max_tokens=1 messages with the entire tool schema
attached. We observed bursts of 250 RPM of these probes from a single
Claude Code Desktop session, exhausting the upstream provider's RPM
quota and starving real traffic on the same channel for ~2 minutes.

Returning a fast 200 here from new-api itself fixes the failure mode
at the source: the SDK is satisfied, no upstream RPM is consumed,
no billing entry is created.

Closes QuantumNous#1694
Closes QuantumNous#2847
Closes QuantumNous#1979

Approach
--------
- Mounted on relayV1Router directly, NOT under httpRouter — the
  former gives us TokenAuth + RouteTag + SystemPerformanceCheck +
  ModelRequestRateLimit; the latter would also pull in
  middleware.Distribute() which selects a channel and starts the
  PreConsume flow. Anthropic defines count_tokens as token-counting
  only with no quota impact, so the bypass is intentional.

- Estimation reuses the project's existing pieces:
  * ClaudeRequest.GetTokenCountMeta() for the canonical flattening
    of system / messages (text, tool_use, tool_result) / tools
  * EstimateTokenByModel() for the Claude-tuned tokenizer that
    /v1/messages itself bills against
  This keeps the count consistent with what /v1/messages would
  report on the same body, so callers can reason about both numbers
  together.

- One small helper, normalizeRequestTools(), converts raw
  map[string]any tool entries (what json.Unmarshal produces when the
  field is `any`) into the typed *dto.Tool / *dto.ClaudeWebSearchTool
  values that dto.ProcessTools requires. Without this every tool
  entry on a count_tokens request would be silently dropped on the
  `default: continue` arm of ProcessTools (dto/claude.go:439-442) and
  the tools text — usually 80%+ of a CLI probe body — would not be
  counted.

- Body parsing uses common.UnmarshalBodyReusable so the request body
  remains available to any logging/observability that runs after
  this handler.

- Image tokens are intentionally not added: getImageToken() needs a
  RelayInfo + http.Request context, which this route doesn't have.
  The CLI probe (the failure mode this PR exists to mitigate) never
  carries images. Documented in the estimator's godoc.

How vs. PR QuantumNous#2384
----------------
QuantumNous#2384 took the deeper path: a new RelayFormat enum value + adaptor
changes + middleware.distributor changes + autoban bypass. That PR
was closed without merge. This PR keeps the surface area small —
one new route, one controller, one estimator, no enum/adaptor
changes, no impact on any code path that doesn't hit the new route.

Files
-----
- router/relay-router.go              + 9 lines  (one route registration)
- controller/claude_count_tokens.go   new       (~35 lines incl. doc)
- service/claude_token_estimator.go   new       (~90 lines)
- service/claude_token_estimator_test.go  new   (12 cases)

Tests
-----
$ go test ./service/ -run 'TestEstimateClaude|TestNormalizeRequestTools' -v
=== RUN   TestEstimateClaudeInputTokens
--- PASS  (10 cases: empty / short user / system+user / CLI probe /
          system as array / CJK / tool_use / tool_result / web-search /
          pre-typed tools)
=== RUN   TestEstimateClaudeInputTokens_NilSafety
--- PASS  (2 cases: nil request / zero-value request)
=== RUN   TestNormalizeRequestTools
--- PASS  (3 cases: nil / non-array / malformed dropped)

Manual verification of the failure mode (sample CLI probe body —
trivial message + Bash tool schema, ~200 bytes):
  EstimateClaudeInputTokens = 43 input tokens
  Endpoint returns: 200 OK {"input_tokens": 43}
  Claude CLI accepts and stops sending fake max_tokens=1 fallback.
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.