Skip to content

feat: add daily token limit for channels#5043

Open
Ylsssq926 wants to merge 4 commits into
QuantumNous:mainfrom
Ylsssq926:feat/channel-daily-token-limit
Open

feat: add daily token limit for channels#5043
Ylsssq926 wants to merge 4 commits into
QuantumNous:mainfrom
Ylsssq926:feat/channel-daily-token-limit

Conversation

@Ylsssq926
Copy link
Copy Markdown

@Ylsssq926 Ylsssq926 commented May 22, 2026

What

Adds a per-channel daily token limit: administrators can configure a daily cap (in raw tokens) on each channel via the existing Channel.Setting JSON. When a channel reaches its configured cap, it is filtered out of routing candidates until usage automatically resets at the server-local midnight.

Closes #2489

Why

OpenAI and several other upstream services hand out a daily free token allowance. Without an in-product cap, operators have no way to "spend the free tier first" - once daily traffic exceeds the free quota the same channel just keeps getting billed. The issue asked for a way to express "channel A: at most 1M tokens per day" without having to manually disable/enable the channel each night.

Design

Storage

  • New optional field daily_token_limit on dto.ChannelSettings (which is what Channel.Setting already serialises to). 0 means unlimited.
  • No DB schema migration. Existing JSON values continue to unmarshal cleanly (the new field is omitempty).

Counter

  • Redis-backed key channel_daily_tokens:{channel_id}:{yyyymmdd}, TTL set to expire at the next server-local midnight via an atomic Lua INCRBY + EXPIREAT script (added as common.RedisIncrByWithExpireAt).
  • No cron / scheduler is required for daily reset - the TTL handles it, which means multiple instances see a consistent reset moment without any leader-election dance.
  • When Redis is enabled but a write fails (transient outage, network partition, ...) we fall back to an in-process counter and emit an explicit warning, so the daily limit cannot be silently bypassed. When Redis is disabled entirely we use the same in-process fallback, with a SysLog warning that this is not multi-instance consistent.

Routing

  • model.GetRandomSatisfiedChannel and model.GetChannel (DB path, used when memory cache is disabled) skip channels that have hit their daily cap.
  • In middleware/distributor.go, an affinity hit on a capped channel falls through to the standard selection path (and the affinity cache for that request is cleared, so the affinity state machine no longer silently swallows the event).
  • A request that explicitly targets a single channel via token binding gets a localised 429 Too Many Requests (i18n key MsgDistributorChannelDailyTokenLimitExceeded, en/zh-CN/zh-TW translations included).
  • We never flip Channel.Status or abilities.enabled for daily limits - those are persistent admin states; daily limits are a soft, time-boxed filter.

Counting

  • Text path (service/text_quota.go): increments by summary.PromptTokens + summary.CompletionTokens on success.
  • WSS / Realtime (service/quota.go): increments by usage.InputTokens + usage.OutputTokens.
  • Audio (service/quota.go): increments by usage.PromptTokens + usage.CompletionTokens.
  • All paths use raw tokens, not quota - model/group ratios must not influence the daily cap. The three paths now agree on semantics (input + output, no audio/cache surcharges).

UI

In web/default's channel edit drawer, "Channel Extra Settings" gets a new "Daily Token Limit" number input (label, description, hint about midnight reset, all i18n'd in en.json / zh.json). The form schema, defaults, parser, and buildSettingJSON are all updated together.

Out of scope (deliberate)

  • Asynchronous task channels (MJ, Suno, video) keep their existing quota-only accounting; counting their tokens needs a separate hook in service/task_billing.go and I'd rather land that as a follow-up than balloon this PR.
  • Multi-key channels are limited at the channel level, not per key. We can add per-key limits later if there's demand.
  • An admin endpoint to surface today's usage. The internal helpers exist; exposing them via the API is a small follow-up PR.

Verification

  • go build ./... and go vet ./... both pass cleanly on the branch.
  • Manual test plan I'd suggest for review:
    1. Create a channel with daily_token_limit = 1000. Issue a request that consumes ~800 tokens - should succeed.
    2. Issue another request that consumes ~300 tokens - succeeds, usage now > limit (this is expected: the cap is enforced pre-routing on the next request, since actual token counts are known only after the upstream replies).
    3. Issue a third request - should be skipped if other channels in the group are available, or get 429 if the request was pinned to this channel via token binding.
    4. Wait until server-local midnight (or change the system clock in a test setup); the channel becomes selectable again.

Commits

  1. feat: add daily token limit for channels - main change
  2. fix(channel-daily-limit): cleanup unused helpers after review
  3. fix(channel-daily-limit): address review feedback - i18n, Redis fallback, affinity log, token alignment
  4. chore(vet): clean up pre-existing go vet warnings - drive-by; feel free to drop this commit if you'd prefer a separate PR

The fourth commit is genuinely a drive-by - go vet ./... already had a few unreachable return statements after panic stubs and a Mutex copied via value receiver. I cleaned them up so vet stays quiet on this branch, but I'm happy to drop that commit and re-submit it as its own PR if you'd prefer.

Risks

  • Multi-instance without Redis: the in-process fallback is per-process, so daily caps drift across replicas. The SysLog warning makes this visible at startup; documentation could call it out more loudly if you'd like.
  • The "last request can overshoot" edge case: because actual token counts arrive after the request, the N-th request that pushes a channel over its limit is allowed to complete; only N+1 is blocked. Strict reservation with refund-on-failure is doable but invasive - left as a follow-up.

Happy to iterate on any of the above.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added per-channel daily token limit enforcement with Redis-backed tracking.
    • Channels with exhausted daily tokens are automatically excluded from selection.
    • Added UI controls to configure daily token limits per channel.
    • Requests to channels exceeding daily limits return HTTP 429 error.
    • Support for multiple languages including English and Chinese.
  • Improvements

    • Enhanced channel affinity handling to respect daily token limits.

Review Change Stack

Ylsssq926 added 4 commits May 22, 2026 11:17
Allow administrators to configure a per-channel daily token cap
(stored in Channel.Setting JSON). When a channel reaches its
configured daily_token_limit, it is filtered out of routing
candidates until usage resets at the server local midnight via
Redis TTL.

- DTO: add DailyTokenLimit to dto.ChannelSettings (no schema migration).
- Counter: Redis-backed channel_daily_tokens:{id}:{yyyymmdd} with
  atomic INCRBY+EXPIREAT, falls back to in-process counter when
  Redis is disabled (single-instance only).
- Routing: GetRandomSatisfiedChannel and ability.GetChannel skip
  channels at their daily cap. Affinity hits also fall through to
  the standard selection path. Token-specified channels return 429
  when their cap is reached.
- Counting: text/audio/realtime success paths increment the daily
  counter using prompt+completion totals (raw tokens, not quota).
- UI: web/default channel edit drawer exposes a 'Daily Token Limit'
  input under Extra Settings.

Closes QuantumNous#2489
- Remove unused getChannelQuery / getPriority in model/ability.go
  after the GetChannel rewrite no longer needed them.
- Fold service.IsChannelDailyTokenAvailable into a thin wrapper
  that delegates to model.IsChannelDailyTokenAvailable, so the
  selection-time logic only has one source of truth.
- Drop service.GetDailyTokenUsage / GetDailyTokenLimit which had
  no callers in the first PR; they can come back when an admin
  endpoint exposing usage is added.
- Localise the daily-limit error path in distributor.go via a new
  i18n key MsgDistributorChannelDailyTokenLimitExceeded so the
  routing layer stays consistent with the rest of the project.
- Make the affinity miss caused by daily-limit observable by
  emitting a structured log line and clearing the cached affinity
  for the request, so the affinity state machine no longer
  silently swallows a meaningful event.
- Fall back to the in-process counter when Redis writes fail in
  IncreaseChannelDailyTokenUsage, and emit an explicit warning so
  the daily limit cannot be silently bypassed by transient Redis
  outages.
- Align WSS / audio counters with the text path by using
  PromptTokens + CompletionTokens (or InputTokens + OutputTokens
  for WSS), so daily limit semantics no longer drift between
  channel types.
This commit is a drive-by cleanup found while making `go vet ./...`
output clean for the daily-token-limit work; reviewers can drop
this commit if they prefer to handle these in a separate PR.

- common/custom-event.go: drop the value-receiver Mutex that vet
  flags as a lock-by-value when CustomEvent is passed around.
- relay/channel/{baidu,cloudflare,cohere,dify,jina,mistral,mokaai,
  palm,tencent,xunfei,zhipu}/adaptor.go: remove unreachable returns
  after `panic("implement me")` stubs that vet's unreachable-code
  check rejects.

No behavioural change.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Walkthrough

This PR implements per-channel daily token usage limits with Redis-backed tracking and in-process fallback. The middleware enforces limits during channel selection, quota services record usage, and channel selection filters by availability. UI forms support configuration, and relay adapters are hardened to panic on unimplemented Claude conversions.

Changes

Per-Channel Daily Token Limit Feature

Layer / File(s) Summary
Core daily token tracking and storage
common/channel_daily_limit.go, common/redis.go, constant/cache_key.go, model/channel_daily_limit.go, service/channel_daily_limit.go
Implements Redis-backed daily counter scoped by channelID and YYYYMMDD, with process-local fallback map protected by mutex; atomic increment via Lua script with EXPIREAT; background cleanup of stale entries; GetChannelDailyTokenUsage, IsChannelDailyTokenUsageAvailable, and IncreaseChannelDailyTokenUsage APIs across layers.
Daily token usage recording in quota services
service/text_quota.go, service/quota.go, types/error.go
Integrates daily token accounting into PostTextConsumeQuota, PostWssConsumeQuota, and PostAudioConsumeQuota after quota consumption; adds ErrorCodeChannelDailyTokenLimitExceeded error code.
Daily token enforcement and channel filtering
middleware/distributor.go, model/ability.go, model/channel_cache.go, service/channel_affinity.go
Middleware enforces daily availability for explicitly selected channels (HTTP 429); filters preferred affinity channels and clears cache when exhausted; GetChannel and GetRandomSatisfiedChannel filter candidate channels by daily availability.
Data models and translations
dto/channel_settings.go, i18n/keys.go, i18n/locales/*.yaml
Adds DailyTokenLimit field to ChannelSettings DTO; defines MsgDistributorChannelDailyTokenLimitExceeded key; provides English and Chinese translations.
Frontend UI and forms
web/default/src/features/channels/types.ts, web/default/src/features/channels/lib/channel-form.ts, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx, web/default/src/i18n/locales/*.json
Adds daily_token_limit to TypeScript channel settings; validates as non-negative in Zod schema; manages form serialization/deserialization; renders numeric input in advanced settings drawer with "0 = unlimited" guidance; provides localized labels.

CustomEvent Mutex Removal

Layer / File(s) Summary
CustomEvent mutex cleanup
common/custom-event.go
Removes unused Mutex sync.Mutex field and sync package import; eliminates lock/unlock calls from WriteContentType.

Claude Conversion Panic Enforcement

Layer / File(s) Summary
ConvertClaudeRequest panic enforcement
relay/channel/baidu/adaptor.go, relay/channel/cloudflare/adaptor.go, relay/channel/cohere/adaptor.go, relay/channel/dify/adaptor.go, relay/channel/jina/adaptor.go, relay/channel/mistral/adaptor.go, relay/channel/mokaai/adaptor.go, relay/channel/palm/adaptor.go, relay/channel/tencent/adaptor.go, relay/channel/xunfei/adaptor.go, relay/channel/zhipu/adaptor.go
Converts unimplemented ConvertClaudeRequest method stubs from silent return nil, nil to explicit panic("implement me") across 11 relay channel adapters; also fixes ConvertGeminiRequest error handling in cohere and removes dead return statement in dify DoResponse.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • QuantumNous/new-api#1274: Both PRs modify middleware/distributor.go's channel selection logic to add platform/feature-specific checks during the Distribute middleware flow.
  • QuantumNous/new-api#2669: Both PRs extend the preferred-channel affinity flow in middleware/distributor.go to handle channel state and availability before selection proceeds.

Suggested reviewers

  • Calcium-Ion
  • seefs001

Poem

🐰 A daily limit springs to life,
Redis counts the tokens bright,
Channels rest when quotas end,
Fallback keeps the ledger sound,
Enforce it fair, no silent waste—
The feature hops another pace! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add daily token limit for channels' clearly and concisely summarizes the primary change—adding a per-channel daily token limit feature.
Linked Issues check ✅ Passed The pull request fully implements the requirement from issue #2489 to add a per-channel single-day token limit with automatic daily reset at midnight.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the daily token limit feature. The exception is multiple ConvertClaudeRequest panic changes across relay adaptors, which appear to be intentional cleanup to expose unimplemented code paths.

✏️ 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@service/text_quota.go`:
- Around line 371-374: The code currently builds dailyTokenUsage from
summary.PromptTokens + summary.CompletionTokens (which may be normalized) before
calling IncreaseChannelDailyTokenUsage; change it to use the raw token total
(e.g. summary.TotalTokens or the original raw usage totals variable) so the
daily counter reflects unnormalized usage—update the construction of
dailyTokenUsage and the call to IncreaseChannelDailyTokenUsage to pass the raw
total instead of the normalized prompt/completion sum.
🪄 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: 3dc27095-d044-4bee-9ef4-acec6e403de3

📥 Commits

Reviewing files that changed from the base of the PR and between f2c7647 and 7da861d.

📒 Files selected for processing (34)
  • common/channel_daily_limit.go
  • common/custom-event.go
  • common/redis.go
  • constant/cache_key.go
  • dto/channel_settings.go
  • i18n/keys.go
  • i18n/locales/en.yaml
  • i18n/locales/zh-CN.yaml
  • i18n/locales/zh-TW.yaml
  • middleware/distributor.go
  • model/ability.go
  • model/channel_cache.go
  • model/channel_daily_limit.go
  • relay/channel/baidu/adaptor.go
  • relay/channel/cloudflare/adaptor.go
  • relay/channel/cohere/adaptor.go
  • relay/channel/dify/adaptor.go
  • relay/channel/jina/adaptor.go
  • relay/channel/mistral/adaptor.go
  • relay/channel/mokaai/adaptor.go
  • relay/channel/palm/adaptor.go
  • relay/channel/tencent/adaptor.go
  • relay/channel/xunfei/adaptor.go
  • relay/channel/zhipu/adaptor.go
  • service/channel_affinity.go
  • service/channel_daily_limit.go
  • service/quota.go
  • service/text_quota.go
  • types/error.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/features/channels/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/zh.json
💤 Files with no reviewable changes (12)
  • relay/channel/jina/adaptor.go
  • relay/channel/tencent/adaptor.go
  • relay/channel/baidu/adaptor.go
  • relay/channel/xunfei/adaptor.go
  • relay/channel/zhipu/adaptor.go
  • relay/channel/cohere/adaptor.go
  • relay/channel/mokaai/adaptor.go
  • common/custom-event.go
  • relay/channel/dify/adaptor.go
  • relay/channel/palm/adaptor.go
  • relay/channel/cloudflare/adaptor.go
  • relay/channel/mistral/adaptor.go

Comment thread service/text_quota.go
Comment on lines +371 to +374
dailyTokenUsage := int64(summary.PromptTokens + summary.CompletionTokens)
if err := IncreaseChannelDailyTokenUsage(relayInfo.ChannelId, dailyTokenUsage); err != nil {
logger.LogError(ctx, "channel daily token usage NOT recorded, daily limit may not take effect: "+err.Error())
}
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 | ⚡ Quick win

Use raw token totals when incrementing daily usage.

This path uses summary.PromptTokens, which can be normalized (reduced) for specific billing semantics, causing daily usage undercount and delayed limit enforcement. Use raw totals (summary.TotalTokens or raw usage totals) for the daily counter.

💡 Suggested fix
-		dailyTokenUsage := int64(summary.PromptTokens + summary.CompletionTokens)
+		dailyTokenUsage := int64(summary.TotalTokens)
 		if err := IncreaseChannelDailyTokenUsage(relayInfo.ChannelId, dailyTokenUsage); err != nil {
 			logger.LogError(ctx, "channel daily token usage NOT recorded, daily limit may not take effect: "+err.Error())
 		}
📝 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
dailyTokenUsage := int64(summary.PromptTokens + summary.CompletionTokens)
if err := IncreaseChannelDailyTokenUsage(relayInfo.ChannelId, dailyTokenUsage); err != nil {
logger.LogError(ctx, "channel daily token usage NOT recorded, daily limit may not take effect: "+err.Error())
}
dailyTokenUsage := int64(summary.TotalTokens)
if err := IncreaseChannelDailyTokenUsage(relayInfo.ChannelId, dailyTokenUsage); err != nil {
logger.LogError(ctx, "channel daily token usage NOT recorded, daily limit may not take effect: "+err.Error())
}
🤖 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 371 - 374, The code currently builds
dailyTokenUsage from summary.PromptTokens + summary.CompletionTokens (which may
be normalized) before calling IncreaseChannelDailyTokenUsage; change it to use
the raw token total (e.g. summary.TotalTokens or the original raw usage totals
variable) so the daily counter reflects unnormalized usage—update the
construction of dailyTokenUsage and the call to IncreaseChannelDailyTokenUsage
to pass the raw total instead of the normalized prompt/completion sum.

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.

对某个渠道进行单日总 token 限制

1 participant