Skip to content

feat(api-keys): self-usage status with limits, shared quota providers, anthropic header policy, and key details page - #14771

Open
fouadSalkini wants to merge 8 commits into
diegosouzapw:release/v3.8.52from
fouadSalkini:feat/api-key-self-usage-status
Open

fouadSalkini wants to merge 8 commits into
diegosouzapw:release/v3.8.52from
fouadSalkini:feat/api-key-self-usage-status

Conversation

@fouadSalkini

@fouadSalkini fouadSalkini commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Makes GET /v1/me/status a 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 upstream anthropic-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 carries used, limit, remaining, utilization (0..1), exceeded, and periodStartAt / resetAt taken 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 (scope self:account-quota):
    • limited to the providers the admin shares with the key (sharedQuotaProviders: null = all reachable providers, the default; [] = none);
    • each account has a label — the connection's display name, with email-like values masked (a raw email is never returned);
    • quotas are served from the provider-limits cache (refresh only when older than 5 min, at most once per connection per 60 s, in-flight deduped), with fetchedAt and stale.
  • generatedAt, and the endpoint now accepts x-api-key as well as Authorization: Bearer.

Per-key settings

New table api_key_self_service_settings (migration 190) with sharedQuotaProviders and anthropicRateLimitHeaders. Missing row/table reads as the defaults (null, "auto"); a corrupt stored list fails closed to []. Managed through PATCH /api/keys/[id] and the new management routes GET / 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 policy

Streaming responses forward every upstream header not on the denylist, so pooled Claude accounts' anthropic-ratelimit-unified-* utilization/status headers and anthropic-organization-id currently 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 to forward to keep the old behavior.

Dashboard

  • API key permissions modal: provider picker for shared account quota (shown while the scope is on) and the header mode (always editable).
  • Each key row links to a new details page /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.
  • 123 new strings, translated into all 66 locales (additive diff).

Tests

  • New: 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; extended tests/unit/api-key-self-service.test.ts and tests/unit/api-key-policy.test.ts (settings merged into apiKeyInfo; env key keeps forwarding — written red first).
  • Focused node suites (self-service, policy, api-manager, /v1/me/status route, 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.
  • i18n: 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)

  • Unit fast-path: 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-only build/mcp-bundle-startup and pack-artifact-policy.
  • Docs Gates: README / AGENTS.md / llm.txt say "183 migrations"; the base already has 185 (this PR adds one more).
  • check:mutation-test-coverage --strict: open-sse/handlers/chatCore/passthroughHelpers.ts → tests/unit/claude-passthrough-empty-response.test.ts missing from stryker.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 tip 66f5b2aa0f).
  • check-key-completeness: bs is missing 16 combos.* keys; zh-TW glossary: combos.advancedHelp.connectionAwareExpansion uses 供應商.

Fixed in this PR after the first CI run: complexity ratchet (dashboard editors split into components/hooks, normalizeDeps reduced to a loader table — per-file violations on touched files are now ≤ base), stryker.conf.json entry for the new header-policy test, and check:agent-skills-sync.

Agent-instruction surface: skills/omni-api-keys/SKILL.md and skills/omni-inference/SKILL.md are regenerated by scripts/skills/generate-agent-skills.mjs --apply from the new OpenAPI operations (two /api/keys/{id}/self-service entries and the updated /v1/me/status description) — generator output only, no hand edits. Per AGENTS.md this needs explicit operator approval before merge.

Notes for reviewers

  • usage.daily/weekly.requests counts every usage_history row (failures included), matching the existing token totals.
  • An unrestricted key (no allowedConnections) reaches every active connection, so with account quota shared it still sees every reachable account; sharedQuotaProviders narrows that by provider.
  • Settings changes apply immediately in the writing process; other processes pick them up within the 30 s settings cache TTL.

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_quota limit source and editor (that module arrives in migration 182 / v3.8.51); everything else is this PR.

  • One Linux release build (14,778 files), shipped byte-identical to the second node (tarball sha256 662b73588ff14117d87d7417c58f2f2aa3a5c51ff59d5512ba85e757d81397d0). Both came up healthy 4 s after the swap; migration applied on each (the prod deploy tree names it 9189_…, a prod-only number, so upstream 189/190 are never shadowed) (api_key_self_service_settings).
  • With a temporary key (self:usage + self:account-quota, pinned to one Claude OAuth connection), deleted afterwards:
    • GET /v1/me/status via x-api-key → 200 with generatedAt, 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, windows session (5h) / weekly (7d), fetchedAt set.
    • Streaming POST /v1/messages (cc/claude-haiku-4-5-20251001, max_tokens: 1): auto mode → 12 anthropic-ratelimit-unified-* headers forwarded; after switching the key to strip → 0.
  • Unauthenticated: /v1/me/status → 401, /api/keys/{id}/self-service → 401; /dashboard/api-manager/{id} → 307 to login.

⚠️ base-red inherited: #15306

fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 24, 2026
fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 24, 2026
…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.
@diegosouzapw diegosouzapw added the protected-surface Touches an agent-instruction surface (AGENTS/CLAUDE/llm.txt/SKILL.md) — per-PR operator OK to merge label Sep 25, 2026
@diegosouzapw

Copy link
Copy Markdown
Owner

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 190 collides with #14801, so whichever lands second must move to 191; (2) with the default auto mode every existing key stops receiving anthropic-ratelimit-* / anthropic-organization-id unless it has self:account-quota and exactly one allowed connection — that's a behaviour change for current clients, so we'd like to confirm it as the default (and check the non-streaming path); (3) the diff touches skills/**/SKILL.md, which needs explicit maintainer sign-off; (4) at 15k lines / 132 files this would be much easier to review split into backend, header policy and dashboard UI — could you consider that? Thanks again!

One coordination note on the migration number: several open PRs claim 190, and the release tip is already at 189. To avoid a duplicate version, please renumber this PR's migration to 194. Slots are handed out in PR age order and merged in ascending order.

@diegosouzapw diegosouzapw changed the title feat(api-keys): self-usage status with limits, shared quota providers, anthropic header policy, and key details page [defer] feat(api-keys): self-usage status with limits, shared quota providers, anthropic header policy, and key details page Sep 25, 2026
@diegosouzapw diegosouzapw added the deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52 label Sep 25, 2026
fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 25, 2026
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).
@fouadSalkini
fouadSalkini force-pushed the feat/api-key-self-usage-status branch from fcf9880 to 65a82d8 Compare September 25, 2026 22:34
@fouadSalkini

Copy link
Copy Markdown
Contributor Author

Thanks @diegosouzapw! Addressed all points and split into 3 stacked PRs:

  1. Renumbered migration 190 → 194 as assigned.
  2. The default for keys without an explicit setting is now "forward", preserving exact legacy behavior for existing keys without changes. The "auto" and "strip" modes are opt-in per key.
  3. Reverted the SKILL.md changes to match base.
  4. Split into 3 independent, stacked PRs:

@fouadSalkini

Copy link
Copy Markdown
Contributor Author

Fixed a stale reference: the settings schema comment in src/shared/validation/schemas/keys.ts now points at migration 194 (194_api_key_self_service_settings.sql). No other references to the old number remain in this PR. api-key-self-service-settings passes 16/16.

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.
fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 26, 2026
…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.
fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 26, 2026
… 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.
@fouadSalkini

Copy link
Copy Markdown
Contributor Author

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 apiKeySelfService.ts split into …Accounts/Limits/Shared, and one-line extensions). Focused suites plus the reset-timing tests pass (143/143) and the branch merges cleanly with release/v3.8.51.

fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 26, 2026
…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.
fouadSalkini added a commit to fouadSalkini/OmniRoute that referenced this pull request Sep 26, 2026
…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>
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.51 to release/v3.8.52 September 29, 2026 11:20
@diegosouzapw

Copy link
Copy Markdown
Owner

Re-homed to release/v3.8.52: v3.8.51 entered its release freeze, so the branch now belongs to the release captain and development continues on the next cycle. Nothing is wrong with this PR — it just needed a live base. No action needed from you; CI will re-run against the new base.

@diegosouzapw diegosouzapw changed the title [defer] feat(api-keys): self-usage status with limits, shared quota providers, anthropic header policy, and key details page feat(api-keys): self-usage status with limits, shared quota providers, anthropic header policy, and key details page Oct 1, 2026
@diegosouzapw diegosouzapw removed the deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52 label Oct 1, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

protected-surface Touches an agent-instruction surface (AGENTS/CLAUDE/llm.txt/SKILL.md) — per-PR operator OK to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants