Defines when each limits-related UI element renders, based on UsageSummary from GET /api/limits/usage.
| Field | Meaning |
|---|---|
config.unlimited |
True when the user has no free-tier cap (Cloudflare-connected, or self-hosted with ENABLE_CLOUDFLARE_LIMITS != 'true'). |
config.limit.maxValue |
Free-tier cap for the current window. Infinity when unlimited (serializes to null). |
config.limit.window |
daily (calendar-aligned UTC) or rolling. |
config.limit.resetAt |
ISO timestamp when the free window resets. |
hasUserToken |
Server has a decrypted Cloudflare OAuth token for the user (HttpOnly cookie). |
hasCloudflareConfigured |
User has selected an account + AI Gateway. |
cloudflareCredits |
Fetched Cloudflare AI Gateway balance (nullable). |
limitCheck.withinLimits |
False when free-tier is exhausted. |
cloudflareConnectEnabled |
Feature flag; when false, connect/limits UI is hidden. |
Cloudflare OAuth tokens are server-side only (HttpOnly cookie). There is no browser-side token check.
Derived state is centralized in useUsageLimitsBadgeState() (src/components/usage-limits-badge.tsx) and reused by the sidebar CTA and auth button.
| Derived flag | Meaning |
|---|---|
hidden |
cloudflareConnectEnabled is false (after load). |
needsConfiguration |
hasUserToken && !hasCloudflareConfigured. |
showCredits |
hasUserToken && hasCloudflareConfigured && cloudflareCredits present. |
showUsage |
config.limit exists AND !config.unlimited. |
isExhausted |
!limitCheck.withinLimits (when free-tier usage is tracked). |
Full-width brand button above the auth row in SidebarFooter. Collapsed sidebar shows glyph-only size-9.
| User state | CTA |
|---|---|
Not authenticated / loading / hidden |
Hidden |
!hasUserToken |
"Connect Cloudflare" → OAuth (/oauth/login) |
needsConfiguration |
"Configure AI Gateway" → /settings |
Configured (hasUserToken && hasCloudflareConfigured) |
Hidden — even if cloudflareCredits is null |
Do not gate this CTA on !showCredits. That incorrectly re-prompts OAuth when the user is configured but the credits payload failed to load.
Uses the same useUsageLimitsBadgeState() hook. Status line under the avatar (sidebar display); limits actions in the account dropdown.
| User state | Status text |
|---|---|
| Loading | "Checking credits..." |
needsConfiguration |
"Configure AI Gateway" (amber) |
showCredits |
$X.XX credits (or with gateway name) |
| Exhausted, no token | "Free limit exhausted" (red) |
| Free tier remaining | usage text (e.g. N free credits left) |
| Otherwise | "Connect Cloudflare" |
| User state | Menu item |
|---|---|
needsConfiguration |
"Configure AI Gateway" → /settings |
showCredits |
"Manage AI Gateway" → /settings |
!hasUserToken (Connect) |
No menu item — Connect lives on the sidebar CTA only |
Settings and Sign Out always remain.
useUsageLimitsBadgeState() is the shared derivation layer. The visual UsageLimitsBadge component is retained but not mounted in the shell (header badge is commented out in global-header.tsx).
| User state | Derived / badge content |
|---|---|
| Loading | loading |
| Token missing, free tier remaining | Connect + showUsage |
| Token present, gateway not selected | needsConfiguration |
| Connected (token + gateway + credits) | showCredits + optional showUsage |
| Connected, free tier exhausted, credits known | showCredits only |
Connected but cloudflareCredits missing |
no showCredits (status may still say Connect; sidebar CTA stays hidden) |
| Free tier exhausted, no token | isExhausted + Connect |
Rules:
- Credit balance requires all three:
hasUserTokenANDhasCloudflareConfiguredANDcloudflareCreditspresent. - Free-tier usage only when
config.limitexists AND!config.unlimited. - Badge click target (if remounted):
/settingswhenneedsConfigurationorshowCredits, otherwise OAuth connect.
| User state | Banner content | CTA |
|---|---|---|
No limitsData / dismissed |
Hidden | — |
config.unlimited + connected (BYOK / feature flag on) |
$X.XX balance · free credits reset in Yh |
Dismiss only |
config.unlimited + not connected (self-hosted) |
Hidden | — |
| Free tier active, not connected | N free credits remaining · resets in Yh |
"Connect" + dismiss |
| Free tier active, connected | Same free-tier text | Dismiss only (no connect CTA) |
| Free tier exhausted, connected | Gateway balance · free credits reset in Yh | Dismiss only |
Threshold: Banner is also hidden when remaining free credits or gateway credits are ≥ CREDITS_BANNER_THRESHOLD (shared/constants/limits.ts, default 10). This is intentional noise reduction and is not part of the popup / connect gating.
Note: Default backend policy (excludeCloudflareConnected: false) keeps connected users on the free-tier counter until exhausted. They only hit the unlimited row when in BYOK mode (excludeBYOKUsers bypass) or when excludeCloudflareConnected is explicitly enabled.
Rules:
- Reset text prefers
config.limit.resetAt(server-provided); falls back to client-side calculation viagetResetDate(window, periodSeconds). rollingwindow withoutresetAtuses the verb "resets within" (upper bound); everything else uses "resets in".- Connect CTA renders only when
!isConnected(whereisConnected = hasUserToken && hasCloudflareConfigured).
Triggered by checkCanSendPrompt (pre-flight) and getBackendLimitDialog (on backend USAGE_LIMIT_EXCEEDED).
| Condition | Dialog | Primary action |
|---|---|---|
| `loading | !limitsData` | |
limitCheck.withinLimits (pre-flight only) |
None | — |
!hasUserToken |
Daily free limit exhausted | "Connect Cloudflare" (OAuth) |
hasUserToken && !hasCloudflareConfigured |
Configure AI Gateway | Navigate to /settings?config_needed=true |
hasUserToken && hasCloudflareConfigured && credits < MINIMUM_CLOUDFLARE_BALANCE |
Insufficient credits ($X.XX) |
Open dash.cloudflare.com/{accountId}/ai/ai-gateway/credits |
| Everything else | None (allow) | — |
MINIMUM_CLOUDFLARE_BALANCE is defined in shared/constants/limits.ts.
Unlike the pre-flight limit popups, this one is backend-error-triggered: a user-account deploy (target: 'user', think behavior) can fail with a structured code on the cloudflare_deployment_error WebSocket message (worker/api/websocketTypes.ts). handle-websocket-message.ts forwards the code via onCloudflareDeployGate; chat.tsx renders the dialog. The toast + chat message still show in all cases.
code |
Emitted when | Dialog | Primary action |
|---|---|---|---|
cloudflare_not_connected |
No decrypted Cloudflare OAuth token for the user | Connect Cloudflare to deploy | OAuth flow (/oauth/login?return_url=...) |
cloudflare_not_configured |
Multiple accounts connected and none selected (a sole connected account is used automatically — deploying needs no AI Gateway) | Select a Cloudflare account | Navigate to /settings?config_needed=true |
| (absent) | Any other deploy failure | None (toast only) | — |
Think deploy targets are gated by the worker flag ENABLE_USER_ACCOUNT_DEPLOY (surfaced to the frontend as userAccountDeploy on GET /api/capabilities): when on, the think deploy button sends target: 'user' ("Deploy to My Account"); when off it sends target: 'platform' ("Deploy") and the backend publishes to the platform dispatch namespace with platform credentials — no Cloudflare connection required, so the popup never fires.
| Behavior | Source |
|---|---|
| BYOK users bypass LLM rate limits | llmConfig.excludeBYOKUsers (default: true) |
| Optionally exempt all connected users from limits | llmConfig.excludeCloudflareConnected (default: false) |
| Free tier uses calendar-daily window (UTC midnight reset) | llmConfig.calendarDaily |
| App-creation limits are independent of LLM limits | Separate appCreation config; not touched by connected/BYOK bypass |
| Limits feature disabled entirely | ENABLE_CLOUDFLARE_LIMITS != 'true' → checkUsageAndBalance returns limit: Infinity immediately |