Repository navigation
[AISAGE-729] fix(ai-gateway): rate-limit signed-in free model use per account - #8456
Conversation
Signed-in callers now count against a separate per-account firewall rule, keyed on the signed kiloUserId in their token. Anonymous callers keep the per-IP rule and get the PROMOTION_MODEL_LIMIT_REACHED code again, which Kilo clients show as a sign-up prompt and do not retry.
…mit retries Signed-in server-side products now count per account like every signed-in caller, so the 401 for anonymous server-side product requests on Cloudflare IPs protected nothing: omitting the feature header already led to the per-IP path. Free-model limit responses also send x-should-retry: false so clients that honor it do not spend more of the hour-long allowance.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe incremental change ( Assumptions: Files Reviewed (2 files)
Previous Review Summaries (2 snapshots, latest commit ef6c97d)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit ef6c97d)Status: No Issues Found | Recommendation: Merge Executive SummaryThe incremental change ( Assumptions: Files Reviewed (2 files)
Previous review (commit 4dfdb6d)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (8 files)
Assumptions: Reviewed by deepseek-flash-latest · Input: 45.5K · Output: 4.9K · Cached: 438.4K Review guidance: REVIEW.md from base branch |
Without a verified bearer token, the free-model limit now waits for the resolved auth: session-cookie callers count per account, and a rejected credential gets the invalid-token response instead of the anonymous sign-up prompt.
The per-account subject now comes from the resolved auth instead of the token claims, so a signature-valid but revoked token gets the invalid-token response instead of spending its account's hourly allowance. Auth has already resolved by this point, so this adds no query.
Summary
Follow-up to #8312. The
free-model-inferencefirewall rule counted every free-model request per IP (ip:<ip>), including requests from signed-in users. That blocked signed-in users who share an IP (NAT, offices), while heavy free use already moves across many IPs. Accounts are fingerprinted at sign-up and can be blocked; IPs are cheap to rotate.free-model-inference-user, keyed onuser:<kiloUserId>. The account comes from the resolved auth, which has already resolved by this point, so the check adds no query. Session-cookie callers count per account too. A rejected credential, including a signature-valid but revoked token, gets the invalid-token response instead of spending the account's allowance or getting the sign-up prompt. This includes server-side products (cloud-agent, code-review, app-builder, gastown), which always authenticate.free-model-inferencerule. When they hit it, the gateway again returnserror.code: "PROMOTION_MODEL_LIMIT_REACHED"with status 429. [AISAGE-729] refactor(gateway): replace free model database rate limit with Vercel Firewall rule #7534 removed this response. Kilo clients still handle it: VS Code shows a sign-up prompt, the CLI shows a sign-up toast, and neither retries the request.x-should-retry: false. The windows are an hour long, so an immediate retry only spends more of the allowance. Kilo CLI v2 and the OpenAI SDKs honor the header.isUserRateLimitedFeaturetoisServerSideProductFeature; its only remaining use is keeping shared Cloudflare egress IPs out of bouncer's payer-sharing signal.Client handling
Kilo clients on
Kilo-Org/kilocodemain:isUnauthorizedPromotionLimitErroraccepts the code with status 401 or 429, andErrorDisplayrenders the sign-up card.parseKiloErrorCodereadserror.code(or a top-levelcode), the TUI shows the sign-up toast, andretryabledoes not retry Kilo error codes. The signed-in 429 has no Kilo code, so it is still retried with backoff.Kilo v2 (
kilo-v2):RateLimiterrors but honorsx-should-retry, hence the header on both limit responses.APIError, so v2 probably shows the response message in the generic error card instead of the sign-up card.Firewall changes
The rule must exist before this deploys.
checkRateLimittreats an unknown rule as "not rate limited", so without it signed-in free-model use is unlimited, and the gateway reportsFirewall rate limit 'free-model-inference-user' is not configuredto Sentry (source: free_model_inference_user).1. Before merging: add the per-account rule
In Vercel, open the
kilocode-ai-gatewayproject → Firewall → Configure → + New Rule, copy the settings of the existing "Free model rate limit" rule, and change the name, ID and limit:Free model rate limit per accountfree-model-inference-userx-vercel-rate-limit-keyThen Review Changes → Publish. After publishing,
GET /v1/security/firewall/config/active?projectId=<kilocode-ai-gateway project id>should list a rule with conditionrate_limit_api_id eq free-model-inference-userandrateLimit: { algo: "fixed_window", keys: ["header:x-vercel-rate-limit-key"], limit: 300, window: 3600 }.2. After this is deployed: lower the anonymous per-IP limit
Edit the existing "Free model rate limit" rule (
free-model-inference): change the request limit from 200 to 100, keeping the 3600 s window, then publish. Do this only after deploying: until then signed-in users still count against this per-IP rule.3. Check after deploying
Model rate limit exceeded, user:<id>, …for signed-in callers andModel rate limit exceeded, ip:<ip>, …only for anonymous callers.Firewall rate limit 'free-model-inference-user' is not configuredreports.Verification
NODE_ENV=test pnpm --filter web exec jestforllm-proxy.test.ts,free-model-rate-limit.test.ts,feature-detection.test.ts, and thebouncersuites: 255 passed (Node 24.14.1).PROMOTION_MODEL_LIMIT_REACHED.llm-proxy.test.tsandfree-model-rate-limit.test.tssuites pass again: 137 tests.x-should-retry: false.pnpm --filter @kilocode/web-shared typecheck: passed.oxlintandoxfmton the changed files: clean.