From 46de00559b7a135c69f5fe229571267b0d028e68 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:20:41 +0800 Subject: [PATCH] feat(web): carry a key's request limits in the Add to ThinkWatch Lite link The rotated-key dialog reads the key's own rate-limit rules and puts the ones Lite can hold exactly into the link's `limits` parameter: enabled, unexpired AI-gateway request rules over whole minutes up to 60 or whole hours up to 24 become Lite's rolling minute/hour windows. Token rules are left out (the gateway counts weighted tokens, Lite plain ones), as are sliding 7-day rules, budgets, MCP rules and the owner's limits. Duplicate windows keep the smaller count; at most 16 windows, the lowest rates. A link with limits needs Lite 2026.10.26, which the dialog states. When the rules cannot be read the link is built without them, as before. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 18 +++ web/src/components/add-to-lite.tsx | 61 +++++--- web/src/components/limits/key-limits-query.ts | 12 ++ web/src/i18n/en.json | 3 +- web/src/i18n/zh.json | 3 +- web/src/lib/lite-link.test.ts | 133 +++++++++++++++++- web/src/lib/lite-link.ts | 73 +++++++++- web/src/routes/api-key-dialogs.tsx | 8 ++ web/src/routes/api-keys.test.tsx | 102 ++++++++++++++ 9 files changed, 392 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fdad7da..8f8b8dc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,24 @@ target. ## [Unreleased] +### Added + +- **The rotated-key dialog's Add to ThinkWatch Lite link carries the key's + request limits.** The key's own enabled, unexpired AI-gateway rules that + count requests over a window of whole minutes up to 60, or whole hours + up to 24 (the console's 1 minute, 5 minutes, 1 hour, 5 hours and 24 + hours among them), go into the link's `limits` parameter as Lite's + rolling windows of the same length; of two rules on one window the + smaller count goes, and at most 16 windows, those allowing the fewest + requests per second. Token rules stay out, since they count weighted + tokens and Lite counts plain ones; so do sliding 7-day rules (Lite's + week is a calendar week), budgets, MCP rules and the owner's limits. + A link with limits needs ThinkWatch Lite 2026.10.26 or later, which the + dialog states under the button. Reading the rules needs + `rate_limits:read` in a scope covering the key; without it, or when the + rules cannot be read, the link is built without limits as before. A new + key has no limits of its own yet, so its link carries none. + ## [3.8.3] — 2026-10-20 Tool-call inspection in streamed answers now decides only on arguments diff --git a/web/src/components/add-to-lite.tsx b/web/src/components/add-to-lite.tsx index a5a31171..33a96a33 100644 --- a/web/src/components/add-to-lite.tsx +++ b/web/src/components/add-to-lite.tsx @@ -1,15 +1,27 @@ import { useId, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { MonitorDown } from 'lucide-react'; +import { useQuery } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { useGatewayUrl } from '@/hooks/use-gateway-url'; import { hasPermission } from '@/lib/api'; -import { LITE_MIN_VERSION, liteDownloadUrl, liteImportLink } from '@/lib/lite-link'; +import { keyRulesQuery } from '@/components/limits/key-limits-query'; +import { + LITE_LIMITS_MIN_VERSION, + LITE_MIN_VERSION, + liteDownloadUrl, + liteImportLink, + liteLimits, +} from '@/lib/lite-link'; import { openAppLink } from '@/lib/open-app-link'; interface AddToLiteProps { /// The plaintext key, as shown on this screen. apiKey: string; + /// The key's id, where the screen has it: the link then carries the + /// key's own request limits, read with `rate_limits:read`. Without the + /// id, the permission, or a readable answer, the link goes without them. + keyId?: string; /// Whether the key may call the AI gateway (`ai_gateway` in its surfaces). aiGateway: boolean; /// Whether the key's owner holds `ai_gateway:use`, without which the @@ -28,15 +40,26 @@ interface AddToLiteProps { /// key on it — with an error page. Lite is a desktop app: below the `md` /// breakpoint (where the console's mobile layout starts) only the Copy /// button shows. -export function AddToLite({ apiKey, aiGateway, ownerMayUseGateway, children }: AddToLiteProps) { +export function AddToLite({ apiKey, keyId, aiGateway, ownerMayUseGateway, children }: AddToLiteProps) { const { t, i18n } = useTranslation(); const { baseUrl, known, failed } = useGatewayUrl(); const [opened, setOpened] = useState(false); const reasonId = useId(); + const limitsId = useId(); const mayUseGateway = ownerMayUseGateway ?? hasPermission('ai_gateway:use'); - const built = known ? liteImportLink(baseUrl, apiKey) : undefined; + // Until the rules come (and when they cannot be read) the link goes + // without limits rather than holding the button back. + const rules = useQuery({ + ...keyRulesQuery(keyId ?? ''), + enabled: !!keyId && aiGateway && hasPermission('rate_limits:read'), + retry: false, + }); + const limits = rules.data ? liteLimits(rules.data) : []; + const built = known ? liteImportLink(baseUrl, apiKey, limits) : undefined; const link = built && 'link' in built ? built.link : undefined; + const withLimits = !!link && limits.length > 0; + const minVersion = withLimits ? LITE_LIMITS_MIN_VERSION : LITE_MIN_VERSION; let reason: string | undefined; if (!aiGateway) { reason = t('lite.reasonNoAiGateway'); @@ -70,6 +93,7 @@ export function AddToLite({ apiKey, aiGateway, ownerMayUseGateway, children }: A