feat(api-keys): self-usage status with limits, shared quota providers, anthropic header policy, and key details page - #14771
Conversation
…gosouzapw#14771 CI gates - Split ApiKeyDetailsPageClient, TokenLimitsEditor, KeyQuotaEditor and SelfServiceQuotaSettings into small components and hooks (no behavior, payload or i18n change) so every function meets the complexity ratchet. - Replace the ternary import chain in normalizeDeps with a table of lazy loaders; a module is still imported only when one of its deps is not injected. - Regenerate skills/omni-api-keys and skills/omni-inference from the new openapi operations (check:agent-skills-sync). - Register tests/unit/anthropic-account-header-policy.test.ts in stryker tap.testFiles. - Link the changelog fragment to diegosouzapw#14771.
|
Thanks @fouadSalkini — this is a thoughtful design (own settings table, fail-closed parsing, masked labels, bounded quota refresh). A few things before we can merge: (1) migration One coordination note on the migration number: several open PRs claim |
…gs endpoints (diegosouzapw#14771 slice 1/3)
The release tip now owns 190_call_logs_content_provenance.sql, so this PR's 190/191 collided. Move to the tentative slots 197/198 (age-order scheme; diegosouzapw#14771 got 194, diegosouzapw#14801 got 196) pending maintainer confirmation, rename the matching test, and update the migration-count claims in README.md, AGENTS.md, llm.txt and its 66 i18n mirrors (regenerated with scripts/i18n/sync-llm-mirrors.mjs).
fcf9880 to
65a82d8
Compare
|
Thanks @diegosouzapw! Addressed all points and split into 3 stacked PRs:
|
|
Fixed a stale reference: the settings schema comment in |
Slice 1/3 copied src/shared/utils/apiKeyPolicy.ts, its test file and stryker.conf.json from an older snapshot, silently undoing base work that landed after that snapshot. Re-apply the base versions and keep only this PR's own additions (self-service settings on apiKeyInfo, the env-key forwarding default, two new policy tests, one stryker test entry). Restored: - apiKeyPolicy.ts: formatResetDurationSuffix, the "Resets in Xh Ym." message suffixes and retryAfter on the budget, token-limit and request-limit 429 responses (diegosouzapw#14188) - tests/unit/api-key-policy.test.ts: readErrorBody, the reset-timing assertions and the "returns the token-limit reset instant" test (diegosouzapw#14188) - stryker.conf.json tap.testFiles: quota-reset-timing and combo-skipped-reset-timing (diegosouzapw#14188), translation-failure-skips-account-cooldown-14815 (diegosouzapw#14830), sudo-password-never-reaches-command-stdin (diegosouzapw#14836)
…ed files Slice 1/3 carried a locally pruned copy of eslint-suppressions.json that dropped entries for files this PR never touches. The CI lint job runs with --pass-on-unpruned-suppressions and prunes stale entries at release reconciliation, so dropping them here is not required. Restore the base file and keep only the removal for src/lib/usage/apiKeySelfService.ts: this PR rewrites that file, the no-restricted-syntax violation is gone, and lint-staged (no pass flag) fails any commit staging a file with an unused suppression. Restored entries: - vertex registry index.ts, executors/vertex.ts, both vscode [token]/combos routes, use-stream-metrics and use-tools-builder tests (diegosouzapw#11247) - executors/vertex.ts, ProxyLogDetail.tsx, analytics/charts.tsx (diegosouzapw#6202) - executor-nlpcloud and qoder-unwrap-error-envelope tests (diegosouzapw#9126) - use-improve-prompt, use-presets, use-stream-metrics, use-structured-output and use-tools-builder tests (diegosouzapw#12144) - gemini-business-provider test (diegosouzapw#11247; base drops it separately)
tests/unit/anthropic-account-header-policy.test.ts only exists from the anthropic header slice (diegosouzapw#14862) on. Listing it here pointed Stryker's tap.testFiles at a missing file whenever this slice lands alone. The entry moves to diegosouzapw#14862.
…iles This slice adds tests/unit/anthropic-account-header-policy.test.ts, so its Stryker tap.testFiles entry belongs here rather than in diegosouzapw#14771, where the file did not exist yet.
… suppression Merging the restored diegosouzapw#14771 suppressions file re-added the entry for tests/unit/gemini-business-provider.test.ts. That file no longer exists and the base already dropped the entry in diegosouzapw#14659. Re-apply the base's removal so this branch differs from release/v3.8.51 only by the apiKeySelfService.ts entry the stack owns.
|
Fixed a split artifact: slice 1/3 was cut from an older snapshot of the monolith branch and silently undid some base changes that landed afterwards. Restored:
Every remaining deletion against the base is this PR's own change (the |
…egosouzapw#14863 API key self-usage Self-usage status with limits and shared quota providers (diegosouzapw#14771), anthropic rate-limit header forwarding policy (diegosouzapw#14862), and the API key details view with self-service quota settings (diegosouzapw#14863), as deployed. The settings migration keeps the deployed number 9189. The header policy default stays "forward" here; the prod-only default follows separately. The chatCore.ts wiring is carried by the agent sessions commit.
…endency loading GET /v1/me/status now imports each default dependency group lazily and only when the caller did not inject it, as in the final diegosouzapw#14771 head. The key_quota loader stays a stub that reports no limits, because db/keyQuota (upstream migration 182) is not on this base. Not ported: the diegosouzapw#14771 head's apiKeyPolicy.ts limit-error text. It is the pre-diegosouzapw#14188 wording and would revert the merged reset hints.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
|
Re-homed to |
Summary
Makes
GET /v1/me/statusa complete self-service view for an API key holder, lets the admin decide which providers' account quota a key may see, adds a per-key policy for the upstreamanthropic-ratelimit-*headers, and gives the dashboard the matching controls plus a per-key details page.GET /v1/me/status(additive — every existing field keeps its shape)usage.daily/usage.weekly(UTC calendar day / ISO week): USD cost (same recorded-cost source as budgets), request count, and a token breakdown (input / output / cache read / cache creation / reasoning / total).limits[]: one entry per enforced per-key limit — daily/weekly USD usage limits, budget, token limits (global / provider / model), key quota (tpm / rpm / monthly USD). Each entry carriesused,limit,remaining,utilization(0..1),exceeded, andperiodStartAt/resetAttaken from that enforcer's own window, so the numbers match what actually blocks the key. A source that fails to read is omitted and logged instead of failing the response.accountQuotas(scopeself:account-quota):sharedQuotaProviders:null= all reachable providers, the default;[]= none);label— the connection's display name, with email-like values masked (a raw email is never returned);fetchedAtandstale.generatedAt, and the endpoint now acceptsx-api-keyas well asAuthorization: Bearer.Per-key settings
New table
api_key_self_service_settings(migration 190) withsharedQuotaProvidersandanthropicRateLimitHeaders. Missing row/table reads as the defaults (null,"auto"); a corrupt stored list fails closed to[]. Managed throughPATCH /api/keys/[id]and the new management routesGET/PUT /api/keys/[id]/self-service(the GET also returns an admin preview of the key's status body and the providers the key can reach).Upstream
anthropic-ratelimit-*header policyStreaming responses forward every upstream header not on the denylist, so pooled Claude accounts'
anthropic-ratelimit-unified-*utilization/status headers andanthropic-organization-idcurrently reach any API key holder. In gateway mode Claude Code acts on those headers (limit warnings, blocking), even though they describe whichever pooled account happened to serve the request.Per key,
anthropicRateLimitHeaders:auto(default): forward only when the key shares account quota (self:account-quota), the serving provider is in its shared providers, and the key is pinned to exactly one connection; otherwise strip.forward/strip: always / never.Requests without an API key and the env key (
OMNIROUTE_API_KEY/ROUTER_API_KEY, the deployment owner) keep forwarding. Non-streaming and error paths are unchanged (they never forwarded these headers).Behavior change: under the default
auto, keys that reach several connections no longer receive the upstream Anthropic rate-limit headers on streaming responses. Set the key toforwardto keep the old behavior.Dashboard
/dashboard/api-manager/[id]: today's and this week's usage, every limit with a utilization bar and reset time, shared account quota cards (masked label, plan, windows, last update, stale badge), and immediate-save editors for key-holder visibility, shared providers + header mode, the USD usage limit, key quota (tpm / rpm / monthly USD) and token limits (add / edit / delete). The token-limit and key-quota APIs existed without any UI before.Tests
tests/unit/api-key-self-service-limits.test.ts,tests/unit/api-key-self-service-accounts.test.ts,tests/unit/api-key-self-service-settings.test.ts,tests/unit/anthropic-account-header-policy.test.ts,tests/unit/api-key-self-usage-dashboard-data.test.ts,tests/unit/ui/api-key-self-usage-dashboard.test.tsx; extendedtests/unit/api-key-self-service.test.tsandtests/unit/api-key-policy.test.ts(settings merged intoapiKeyInfo; env key keeps forwarding — written red first)./v1/me/statusroute, streaming header strip / budget / fix(sse): Codex quota headers leak the selected pool/combo account's quota to the caller #14116 leak tests): 155/155 pass. Vitest UI (api-key-self-usage-dashboard,api-manager-loading-status-12066): 9/9. Migration suites (numbering, uniqueness, runner): 77/77.npm run typecheck:core,check-dashboard-typecheck, ESLint (with suppressions), Prettier,check:cycles,check-db-rules,check:openapi-routes,check:openapi-coverage(702/718 → documents the new routes),check:any-budget:t11,check-docs-sync,check-migration-numbering: clean.check-ui-keys-coverage,check-translation-ratio,check-ui-value-drift,check-new-key-coverage, zh-CN glossary: pass.Inherited failures (reproduced on a clean worktree of the base tip
3bfe5fe8a2, same counts — not introduced here)settings-i18n-keys(1),proxyfetch-upstream-status-capture(1),chatcore-upstream-timeouts(1),combo-responses-sse-failure-fallback(4),i18n-glossary-consistency-check(2),check-docs-counts-sync(1); CI-onlybuild/mcp-bundle-startupandpack-artifact-policy.check:mutation-test-coverage --strict:open-sse/handlers/chatCore/passthroughHelpers.ts→tests/unit/claude-passthrough-empty-response.test.tsmissing fromstryker.conf.json.check-file-size:src/sse/handlers/chat.ts,src/sse/services/auth.ts,open-sse/executors/default.ts,open-sse/translator/response/openai-responses.ts(same sizes on the base tip66f5b2aa0f).check-key-completeness:bsis missing 16combos.*keys; zh-TW glossary:combos.advancedHelp.connectionAwareExpansionuses 供應商.Fixed in this PR after the first CI run: complexity ratchet (dashboard editors split into components/hooks,
normalizeDepsreduced to a loader table — per-file violations on touched files are now ≤ base),stryker.conf.jsonentry for the new header-policy test, andcheck:agent-skills-sync.Agent-instruction surface:
skills/omni-api-keys/SKILL.mdandskills/omni-inference/SKILL.mdare regenerated byscripts/skills/generate-agent-skills.mjs --applyfrom the new OpenAPI operations (two/api/keys/{id}/self-serviceentries and the updated/v1/me/statusdescription) — generator output only, no hand edits. Per AGENTS.md this needs explicit operator approval before merge.Notes for reviewers
usage.daily/weekly.requestscounts everyusage_historyrow (failures included), matching the existing token totals.allowedConnections) reaches every active connection, so with account quota shared it still sees every reachable account;sharedQuotaProvidersnarrows that by provider.Live validation (Hard Rule #18)
Deployed ahead of merge to both operator nodes. They run a v3.8.50-based tree, so the deploy variant omits the
key_quotalimit source and editor (that module arrives in migration 182 / v3.8.51); everything else is this PR.662b73588ff14117d87d7417c58f2f2aa3a5c51ff59d5512ba85e757d81397d0). Both came up healthy 4 s after the swap; migration applied on each (the prod deploy tree names it9189_…, a prod-only number, so upstream 189/190 are never shadowed) (api_key_self_service_settings).self:usage+self:account-quota, pinned to one Claude OAuth connection), deleted afterwards:GET /v1/me/statusviax-api-key→ 200 withgeneratedAt,usage.daily(2026-09-24T00:00Z → 2026-09-25T00:00Z),usage.weekly(Monday 2026-09-21T00:00Z),limits: [], and one account quota: a masked account label, windowssession (5h)/weekly (7d),fetchedAtset.POST /v1/messages(cc/claude-haiku-4-5-20251001,max_tokens: 1):automode → 12anthropic-ratelimit-unified-*headers forwarded; after switching the key tostrip→ 0./v1/me/status→ 401,/api/keys/{id}/self-service→ 401;/dashboard/api-manager/{id}→ 307 to login.