diff --git a/.env.example b/.env.example
index bf8bf769ae..de212a773d 100644
--- a/.env.example
+++ b/.env.example
@@ -2813,3 +2813,13 @@ QUOTA_STORE_DRIVER=sqlite
# Spokesperson (Faro) base URL for the dashboard chat proxy (/api/conductor/ask).
# Used by: src/lib/conductor/faroProxy.ts
# CONDUCTOR_SPOKESPERSON_URL=http://127.0.0.1:7920
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# QUOTA-AWARE PROVIDER SCHEDULING (opt-in, Phase 2)
+# ═══════════════════════════════════════════════════════════════════════════════
+# When enabled, routing skips connections whose configured per-window token
+# budget (rateLimitOverrides.tpm) cannot afford the estimated request cost —
+# before dispatching — instead of waiting for a 429. Fail-open: connections
+# without a configured budget are always considered affordable. Requires the
+# provider_quota_state table (migration 148).
+# OMNIROUTE_QUOTA_AWARE_ROUTING=0
diff --git a/AGENTS.md b/AGENTS.md
index 1ac759c0bc..9cfcf5aea6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below.
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
-| Database | `src/lib/db/` | SQLite domain modules (150 migrations) |
+| Database | `src/lib/db/` | SQLite domain modules (151 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
diff --git a/README.md b/README.md
index 607c411121..6211c39ed5 100644
--- a/README.md
+++ b/README.md
@@ -1150,7 +1150,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c
| Runtime | Node.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 |
| Language | TypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) |
| Framework | Next.js 16 + React 19 + Tailwind CSS 4 |
- | Database | better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 117 domain modules, 150 migrations |
+ | Database | better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 117 domain modules, 151 migrations |
| Memory | SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay |
| Schemas | Zod 4 — MCP tool I/O validation + API contracts |
| Protocols | MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) |
diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt
index 0e9595dd23..513109bd93 100644
--- a/docs/i18n/ar/llm.txt
+++ b/docs/i18n/ar/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt
index 7e7013db28..132a2b95d3 100644
--- a/docs/i18n/az/llm.txt
+++ b/docs/i18n/az/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt
index 7e7013db28..132a2b95d3 100644
--- a/docs/i18n/bg/llm.txt
+++ b/docs/i18n/bg/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt
index 3247e4efaa..32a28989d3 100644
--- a/docs/i18n/bn/llm.txt
+++ b/docs/i18n/bn/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt
index aa26c9d307..cfed4729a2 100644
--- a/docs/i18n/cs/llm.txt
+++ b/docs/i18n/cs/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt
index 92026bec0a..96d4df3ba5 100644
--- a/docs/i18n/da/llm.txt
+++ b/docs/i18n/da/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt
index 2bb8879645..5d6d5b4bda 100644
--- a/docs/i18n/de/llm.txt
+++ b/docs/i18n/de/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt
index 8393a6352c..5d62efc3c4 100644
--- a/docs/i18n/es/llm.txt
+++ b/docs/i18n/es/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt
index 6112eb7470..ff28036917 100644
--- a/docs/i18n/fa/llm.txt
+++ b/docs/i18n/fa/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt
index 4e47eaae21..4eff7ebfb1 100644
--- a/docs/i18n/fi/llm.txt
+++ b/docs/i18n/fi/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt
index 106ed41699..9e7aa6153a 100644
--- a/docs/i18n/fr/llm.txt
+++ b/docs/i18n/fr/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt
index c25ec09af1..622ff00a3c 100644
--- a/docs/i18n/gu/llm.txt
+++ b/docs/i18n/gu/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt
index c819680d65..742336415c 100644
--- a/docs/i18n/he/llm.txt
+++ b/docs/i18n/he/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt
index 380e7ec2c1..55089a6fe2 100644
--- a/docs/i18n/hi/llm.txt
+++ b/docs/i18n/hi/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt
index 1eea21dd51..564ab17955 100644
--- a/docs/i18n/hu/llm.txt
+++ b/docs/i18n/hu/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt
index dafa9c0a83..a831cb94c7 100644
--- a/docs/i18n/id/llm.txt
+++ b/docs/i18n/id/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt
index e7a8175d67..b1e99658c6 100644
--- a/docs/i18n/in/llm.txt
+++ b/docs/i18n/in/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt
index d19aad5335..4c8c0fa488 100644
--- a/docs/i18n/it/llm.txt
+++ b/docs/i18n/it/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt
index 4581d48a8b..2957bb5d24 100644
--- a/docs/i18n/ja/llm.txt
+++ b/docs/i18n/ja/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt
index c8f7c33585..676027c459 100644
--- a/docs/i18n/ko/llm.txt
+++ b/docs/i18n/ko/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt
index b2c7bcb398..712ca57532 100644
--- a/docs/i18n/mr/llm.txt
+++ b/docs/i18n/mr/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt
index 40afc36364..b8f8377d5d 100644
--- a/docs/i18n/ms/llm.txt
+++ b/docs/i18n/ms/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt
index c36b126f73..273b8f660a 100644
--- a/docs/i18n/nl/llm.txt
+++ b/docs/i18n/nl/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt
index 089b364abb..f4088e4669 100644
--- a/docs/i18n/no/llm.txt
+++ b/docs/i18n/no/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt
index c8c3fd60ee..813576b497 100644
--- a/docs/i18n/phi/llm.txt
+++ b/docs/i18n/phi/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt
index 656e1742fc..2857c53344 100644
--- a/docs/i18n/pl/llm.txt
+++ b/docs/i18n/pl/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt
index 5c9a3d8a7a..a8a17c62c0 100644
--- a/docs/i18n/pt-BR/llm.txt
+++ b/docs/i18n/pt-BR/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt
index 4c6dba527a..f20a66a34d 100644
--- a/docs/i18n/pt/llm.txt
+++ b/docs/i18n/pt/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt
index a5f5c4d00a..2687e58613 100644
--- a/docs/i18n/ro/llm.txt
+++ b/docs/i18n/ro/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt
index de3deff289..d73b026d0d 100644
--- a/docs/i18n/ru/llm.txt
+++ b/docs/i18n/ru/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt
index 8cba00bbac..93296c2dd9 100644
--- a/docs/i18n/sk/llm.txt
+++ b/docs/i18n/sk/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt
index fdfa98b483..b595357f03 100644
--- a/docs/i18n/sv/llm.txt
+++ b/docs/i18n/sv/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt
index 4c44968c4d..f5611b670f 100644
--- a/docs/i18n/sw/llm.txt
+++ b/docs/i18n/sw/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt
index e293ffa1bb..57b50125ca 100644
--- a/docs/i18n/ta/llm.txt
+++ b/docs/i18n/ta/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt
index 84645b5600..957a06d022 100644
--- a/docs/i18n/te/llm.txt
+++ b/docs/i18n/te/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt
index 0193696d1d..c6d87ee285 100644
--- a/docs/i18n/th/llm.txt
+++ b/docs/i18n/th/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt
index 4401f086d6..dc9ba78355 100644
--- a/docs/i18n/tr/llm.txt
+++ b/docs/i18n/tr/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt
index bb28ccb28d..72577e12ae 100644
--- a/docs/i18n/uk-UA/llm.txt
+++ b/docs/i18n/uk-UA/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt
index c95e62602d..7683907b24 100644
--- a/docs/i18n/ur/llm.txt
+++ b/docs/i18n/ur/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt
index 130c17af64..fe9539ff36 100644
--- a/docs/i18n/vi/llm.txt
+++ b/docs/i18n/vi/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt
index 4b7272323d..3a172fef87 100644
--- a/docs/i18n/zh-CN/llm.txt
+++ b/docs/i18n/zh-CN/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt
index 55c36bb840..4a8da77a2b 100644
--- a/docs/i18n/zh-TW/llm.txt
+++ b/docs/i18n/zh-TW/llm.txt
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md
index 2cd20a5542..ba1c50f0ba 100644
--- a/docs/reference/ENVIRONMENT.md
+++ b/docs/reference/ENVIRONMENT.md
@@ -1550,3 +1550,11 @@ Long-lived SSE consumer that mirrors OmniConductor hub tasks into the local A2A
| `CONDUCTOR_HUB_TOKEN` | _(empty)_ | `src/lib/conductor/boot.ts` | Hub credential for the SSE feed — emit a `spokesperson`-kind peer on the hub (`POST /v1/peers`, admin). |
| `CONDUCTOR_ORCHESTRATOR_TOKEN` | _(empty)_ | `src/lib/conductor/hubProxy.ts` | Credential for inbound A2A→hub task delegation (`POST /v1/tasks`); falls back to `CONDUCTOR_HUB_TOKEN` when unset. |
| `CONDUCTOR_SPOKESPERSON_URL` | `http://127.0.0.1:7920` | `src/lib/conductor/faroProxy.ts` | Base URL of the spokesperson (Faro) service behind the dashboard chat proxy (`/api/conductor/ask`). |
+
+### Quota-aware scheduling
+
+Used by `open-sse/services/combo.ts` and `src/lib/quota/quotaScheduler.ts` for pre-request token-budget checks. Opt-in — default routing behavior is unchanged when unset.
+
+| Variable | Default | Source File | Description |
+| --------------------------------- | -------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
+| `OMNIROUTE_QUOTA_AWARE_ROUTING` | `0` | `open-sse/services/combo.ts` | When `1`, skip connections whose per-window token budget (`rateLimitOverrides.tpm`, table `provider_quota_state`) cannot afford the estimated request cost before dispatch. Fail-open when no budget configured. |
diff --git a/llm.txt b/llm.txt
index f99c209828..d7afd1f044 100644
--- a/llm.txt
+++ b/llm.txt
@@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 151 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 151 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts
index df86a79375..f8b2333f46 100644
--- a/open-sse/services/combo.ts
+++ b/open-sse/services/combo.ts
@@ -86,7 +86,29 @@ import {
import { selectQuotaShareTarget } from "./combo/quotaShareStrategy.ts";
import { makeConnectionConcurrencyResolver, lookupPositiveCap } from "./combo/concurrencyCaps.ts";
import { acquireQuotaShareConcurrencySlot } from "./combo/quotaShareConcurrency.ts";
+import { canAffordRequest } from "../../src/lib/quota/quotaScheduler.ts";
+import { getCachedProviderConnectionById } from "../../src/lib/localDb.js";
import { orderTargetsByEvalScores } from "./evalRouting.ts";
+
+/**
+ * Resolve the configured per-connection token budget (rateLimitOverrides.tpm)
+ * for quota reservation. Returns undefined when unconfigured — the store then
+ * keeps the previously recorded limit (or 0 for a fresh row, meaning "no
+ * budget enforced").
+ */
+function resolveTargetTokenLimit(target: { connectionId?: string | null }): number | undefined {
+ const connectionId = target?.connectionId;
+ if (!connectionId) return undefined;
+ try {
+ const connection = getCachedProviderConnectionById(connectionId);
+ const overrides = (connection as { rateLimitOverrides?: Record | null } | null)
+ ?.rateLimitOverrides;
+ const tpm = overrides?.tpm;
+ return typeof tpm === "number" && tpm > 0 ? tpm : undefined;
+ } catch {
+ return undefined;
+ }
+}
import {
applyPromptCacheAffinity,
expandPromptCacheAffinityTargets,
@@ -1069,6 +1091,27 @@ export async function handleComboChat({
}
}
+ // Quota-aware scheduling (opt-in, OMNIROUTE_QUOTA_AWARE_ROUTING=1):
+ // when a per-connection token budget is configured (provider_quota_state),
+ // skip targets whose remaining budget cannot afford this request —
+ // BEFORE dispatching — instead of waiting for a 429. Fails open: when
+ // no budget is configured the decision is always affordable.
+ if (process.env.OMNIROUTE_QUOTA_AWARE_ROUTING === "1" && provider && target.connectionId) {
+ const quotaDecision = canAffordRequest(
+ target.connectionId,
+ modelStr,
+ body as Record | null | undefined
+ );
+ if (!quotaDecision.affordable) {
+ log.info(
+ "COMBO",
+ `Skipping ${modelStr} — quota budget ${quotaDecision.reason} (remaining ${quotaDecision.tokensRemaining ?? 0}, cost ${quotaDecision.estimatedCost ?? 0})`
+ );
+ if (i > 0) fallbackCount++;
+ return null;
+ }
+ }
+
// Pre-screen snapshot is NOT used as a permanent skip — availability
// is always re-checked via isModelAvailable below because connection
// cooldowns can expire between setTry retries, making a previously
@@ -1109,6 +1152,7 @@ export async function handleComboChat({
if (i > 0) fallbackCount++;
return stopProtectedPriorityTarget(`Connection capacity reached for ${modelStr}`);
}
+
}
// Retry loop for transient errors
@@ -2865,6 +2909,25 @@ async function handleRoundRobinCombo({
failoverBeforeRetry: config.failoverBeforeRetry,
});
+ // Quota-aware scheduling: reserve the estimated budget for this
+ // dispatch (opt-in, same env gate as the pre-request check). Best-effort
+ // and non-blocking — recording must never break the request path.
+ if (
+ process.env.OMNIROUTE_QUOTA_AWARE_ROUTING === "1" &&
+ target.connectionId &&
+ attemptBody &&
+ typeof attemptBody === "object"
+ ) {
+ try {
+ const { reserveQuota } = await import("../../src/lib/quota/quotaScheduler.ts");
+ reserveQuota(target.connectionId, modelStr, attemptBody as Record, {
+ tokenLimit: resolveTargetTokenLimit(target),
+ });
+ } catch {
+ // best-effort only
+ }
+ }
+
// Success — validate response quality before returning
if (result.ok) {
let rrClone: Response;
diff --git a/scripts/check/check-migration-numbering.mjs b/scripts/check/check-migration-numbering.mjs
index b377c1af74..86799b51b3 100644
--- a/scripts/check/check-migration-numbering.mjs
+++ b/scripts/check/check-migration-numbering.mjs
@@ -45,17 +45,13 @@ export const KNOWN_DUPLICATE_VERSIONS = new Set([
// Fonte: auditoria do disco (src/lib/db/migrations/). Além dos slots legados,
// As migrations Radar 144–145, a migration 143 e a 147 já aterrissaram. O job
// registry foi promovido de 139 para 146 pela tabela
-// RENAMED_MIGRATION_COMPATIBILITY. A 149 aterrissa junto com #10066
-// (149_api_key_combo_access.sql). 148 permanece reservada por PRs #10001 e
-// #10047 ainda em trânsito. O stale-enforcement exige que cada reserva seja
-// removida quando os arquivos correspondentes aterrissarem na release.
+// RENAMED_MIGRATION_COMPATIBILITY. A 148 aterrissou nesta branch
+// (148_provider_quota_state.sql) e a 149 aterrissou junto com #10066
+// (149_api_key_combo_access.sql) — nenhuma das duas é mais um gap. O
+// stale-enforcement exige que cada reserva seja removida quando os arquivos
+// correspondentes aterrissarem na release.
// ---------------------------------------------------------------------------
-export const KNOWN_GAPS = new Set([
- "026",
- "055",
- "121", // número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12)
- "148", // reserved by open PRs #10001 and #10047
-]);
+export const KNOWN_GAPS = new Set(["026", "055", "121"]); // 121: número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12); 144/145 aterrissaram na release (radar offers/intel cache), 148/149 aterrissaram (provider_quota_state, api_key_combo_access)
function pad3(n) {
return String(n).padStart(3, "0");
diff --git a/src/app/api/settings/quota/state/route.ts b/src/app/api/settings/quota/state/route.ts
new file mode 100644
index 0000000000..a47128afed
--- /dev/null
+++ b/src/app/api/settings/quota/state/route.ts
@@ -0,0 +1,102 @@
+/**
+ * /api/settings/quota/state — Dashboard visibility endpoint for provider quota states.
+ *
+ * GET: Returns live quota states, reset timers, and aggregated usage analytics.
+ * POST: Resets expired quota windows or purges a specific connection quota record.
+ *
+ * Auth: requireManagementAuth (dashboard session, manage-scope API key, or local CLI token).
+ * Sanitization: all error responses via buildErrorBody (Hard Rule #12).
+ * Validation: POST body validated with Zod.
+ *
+ * Part of: Quota-aware provider scheduling (Phase 2).
+ */
+
+import { NextResponse } from "next/server";
+import { z } from "zod";
+import { buildErrorBody } from "@omniroute/open-sse/utils/error";
+import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
+import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
+import { getQuotaAnalyticsSummary } from "@/lib/quota/quotaAnalytics";
+import { getActiveQuotaResetItems, resetExpiredQuotaWindows } from "@/lib/quota/quotaResetTimers";
+import { clearProviderQuota } from "@/lib/quota/providerQuotaState";
+
+const QuotaStateActionSchema = z.discriminatedUnion("action", [
+ z.object({ action: z.literal("reset_expired") }),
+ z.object({
+ action: z.literal("clear_connection"),
+ connectionId: z.string().min(1),
+ model: z.string().min(1),
+ }),
+]);
+
+export const dynamic = "force-dynamic";
+
+export async function OPTIONS() {
+ return handleCorsOptions();
+}
+
+export async function GET(request: Request) {
+ const authError = await requireManagementAuth(request);
+ if (authError) return authError;
+
+ try {
+ const analytics = getQuotaAnalyticsSummary();
+ const resetTimers = getActiveQuotaResetItems();
+
+ return NextResponse.json(
+ {
+ success: true,
+ analytics,
+ resetTimers,
+ timestamp: new Date().toISOString(),
+ },
+ { headers: CORS_HEADERS }
+ );
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Failed to read quota state";
+ return NextResponse.json(buildErrorBody(500, message), {
+ status: 500,
+ headers: CORS_HEADERS,
+ });
+ }
+}
+
+export async function POST(request: Request) {
+ const authError = await requireManagementAuth(request);
+ if (authError) return authError;
+
+ try {
+ const body = await request.json().catch(() => null);
+ const parsed = QuotaStateActionSchema.safeParse(body);
+ if (!parsed.success) {
+ return NextResponse.json(buildErrorBody(400, parsed.error.message), {
+ status: 400,
+ headers: CORS_HEADERS,
+ });
+ }
+
+ const { action } = parsed.data;
+
+ if (action === "reset_expired") {
+ const resetCount = resetExpiredQuotaWindows();
+ return NextResponse.json(
+ { success: true, resetCount, message: `Reset ${resetCount} expired quota windows.` },
+ { headers: CORS_HEADERS }
+ );
+ }
+
+ // action === "clear_connection"
+ const { connectionId, model } = parsed.data;
+ clearProviderQuota(connectionId);
+ return NextResponse.json(
+ { success: true, message: `Cleared quota state for connection ${connectionId} (${model}).` },
+ { headers: CORS_HEADERS }
+ );
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Failed to update quota state";
+ return NextResponse.json(buildErrorBody(500, message), {
+ status: 500,
+ headers: CORS_HEADERS,
+ });
+ }
+}
diff --git a/src/lib/db/migrations/148_provider_quota_state.sql b/src/lib/db/migrations/148_provider_quota_state.sql
new file mode 100644
index 0000000000..12ad9ecdb0
--- /dev/null
+++ b/src/lib/db/migrations/148_provider_quota_state.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS provider_quota_state (
+ connection_id TEXT NOT NULL,
+ model TEXT NOT NULL,
+ tokens_used INTEGER NOT NULL DEFAULT 0,
+ token_limit INTEGER NOT NULL DEFAULT 0,
+ window_start INTEGER NOT NULL,
+ window_reset INTEGER NOT NULL,
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
+ PRIMARY KEY (connection_id, model)
+);
+CREATE INDEX IF NOT EXISTS idx_pqs_connection ON provider_quota_state(connection_id);
+CREATE INDEX IF NOT EXISTS idx_pqs_reset ON provider_quota_state(window_reset) WHERE window_reset IS NOT NULL;
diff --git a/src/lib/quota/providerQuotaState.ts b/src/lib/quota/providerQuotaState.ts
new file mode 100644
index 0000000000..234066ff4b
--- /dev/null
+++ b/src/lib/quota/providerQuotaState.ts
@@ -0,0 +1,201 @@
+/**
+ * providerQuotaState.ts — per-connection token budget ledger.
+ *
+ * Tracks tokens used against a configured per-minute (or per-window) token
+ * limit for a (connection, model) pair. The purpose is PRE-REQUEST capacity
+ * awareness: before dispatching to a provider, the scheduler can ask "does
+ * this connection have budget left?" and skip exhausted connections instead
+ * of waiting for a 429.
+ *
+ * Design notes:
+ * - Window semantics: fixed windows keyed by `window_start` (epoch ms).
+ * When `window_reset` passes, usage resets to 0 for the new window.
+ * - Fail-open: reads return `{ known: false }` when the store is missing
+ * or empty — the scheduler treats unknown budget as available (existing
+ * routing behavior is preserved when quota tracking is not configured).
+ * - Writes are best-effort: recording usage must never break the request
+ * path (catch + log + return).
+ *
+ * Part of: Quota-aware provider scheduling (feat/quota-aware-scheduling).
+ */
+import { getDbInstance } from "@/lib/db/core";
+import { createLogger } from "@/shared/utils/logger";
+
+const log = createLogger("quota:provider-state");
+
+export interface ProviderQuotaRow {
+ connectionId: string;
+ model: string;
+ tokensUsed: number;
+ tokenLimit: number;
+ windowStart: number;
+ windowReset: number;
+ updatedAt: string;
+}
+
+export interface ProviderQuotaSnapshot {
+ /** true when the store has a fresh record for this window */
+ known: boolean;
+ tokensUsed: number;
+ tokenLimit: number;
+ /** remaining tokens in the current window (clamped >= 0) */
+ tokensRemaining: number;
+ /** 0..1 ratio of the window budget still available */
+ remainingRatio: number;
+ windowReset: number;
+}
+
+interface RowLike {
+ connection_id?: string;
+ model?: string;
+ tokens_used?: number;
+ token_limit?: number;
+ window_start?: number;
+ window_reset?: number;
+ updated_at?: string;
+}
+
+function normalizeRow(row: RowLike): ProviderQuotaRow {
+ return {
+ connectionId: String(row.connection_id ?? ""),
+ model: String(row.model ?? ""),
+ tokensUsed: Number(row.tokens_used ?? 0),
+ tokenLimit: Number(row.token_limit ?? 0),
+ windowStart: Number(row.window_start ?? 0),
+ windowReset: Number(row.window_reset ?? 0),
+ updatedAt: String(row.updated_at ?? ""),
+ };
+}
+
+/**
+ * Read the current quota snapshot for (connectionId, model).
+ * When the record is stale (its window expired) the caller sees
+ * `known: false` — usage for the new window is implicitly zero.
+ */
+export function getProviderQuota(
+ connectionId: string,
+ model: string
+): ProviderQuotaSnapshot | null {
+ if (!connectionId || !model) return null;
+ try {
+ const db = getDbInstance();
+ const row = db
+ .prepare("SELECT * FROM provider_quota_state WHERE connection_id = ? AND model = ?")
+ .get(connectionId, model) as RowLike | undefined;
+ if (!row) return null;
+
+ const normalized = normalizeRow(row);
+ const now = Date.now();
+ if (normalized.windowReset > 0 && now > normalized.windowReset) {
+ return {
+ known: false,
+ tokensUsed: 0,
+ tokenLimit: 0,
+ tokensRemaining: 0,
+ remainingRatio: 1,
+ windowReset: normalized.windowReset,
+ };
+ }
+
+ const tokenLimit = normalized.tokenLimit > 0 ? normalized.tokenLimit : 0;
+ const tokensUsed = Math.max(0, normalized.tokensUsed);
+ const tokensRemaining = tokenLimit > 0 ? Math.max(0, tokenLimit - tokensUsed) : 0;
+ const remainingRatio =
+ tokenLimit > 0 ? Math.min(1, Math.max(0, tokensRemaining / tokenLimit)) : 1;
+
+ return {
+ known: true,
+ tokensUsed,
+ tokenLimit,
+ tokensRemaining,
+ remainingRatio,
+ windowReset: normalized.windowReset,
+ };
+ } catch (err) {
+ log.warn(
+ { err: (err as Error)?.message, connectionId, model },
+ "getProviderQuota failed — fail-open"
+ );
+ return null;
+ }
+}
+
+/**
+ * Record token usage for (connectionId, model) in the current window.
+ *
+ * If no row exists, seeds one with the configured tokenLimit. If the window
+ * has rolled over, resets usage to the new usage. Best-effort: never throws.
+ */
+export function recordProviderQuotaUsage(
+ connectionId: string,
+ model: string,
+ tokensUsedDelta: number,
+ opts: { tokenLimit?: number; windowMs?: number } = {}
+): void {
+ if (!connectionId || !model || !(tokensUsedDelta > 0)) return;
+ try {
+ const db = getDbInstance();
+ const existing = db
+ .prepare("SELECT * FROM provider_quota_state WHERE connection_id = ? AND model = ?")
+ .get(connectionId, model) as RowLike | undefined;
+
+ const now = Date.now();
+ const windowMs = opts.windowMs ?? 60_000; // default: per-minute window
+ const windowStart = Math.floor(now / windowMs) * windowMs;
+ const windowReset = windowStart + windowMs;
+
+ if (!existing) {
+ db.prepare(
+ `INSERT OR REPLACE INTO provider_quota_state
+ (connection_id, model, tokens_used, token_limit, window_start, window_reset, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
+ ).run(
+ connectionId,
+ model,
+ tokensUsedDelta,
+ opts.tokenLimit ?? 0,
+ windowStart,
+ windowReset,
+ new Date().toISOString()
+ );
+ return;
+ }
+
+ const normalized = normalizeRow(existing);
+ const windowRolledOver = normalized.windowReset > 0 && now > normalized.windowReset;
+ const nextUsed = windowRolledOver ? tokensUsedDelta : normalized.tokensUsed + tokensUsedDelta;
+ const nextLimit =
+ opts.tokenLimit && opts.tokenLimit > 0 ? opts.tokenLimit : normalized.tokenLimit;
+
+ db.prepare(
+ `UPDATE provider_quota_state
+ SET tokens_used = ?, token_limit = ?, window_start = ?, window_reset = ?, updated_at = ?
+ WHERE connection_id = ? AND model = ?`
+ ).run(
+ nextUsed,
+ nextLimit,
+ windowStart,
+ windowReset,
+ new Date().toISOString(),
+ connectionId,
+ model
+ );
+ } catch (err) {
+ log.warn(
+ { err: (err as Error)?.message, connectionId, model },
+ "recordProviderQuotaUsage failed — best-effort"
+ );
+ }
+}
+
+/** Delete all quota state for a connection (used on connection removal). */
+export function clearProviderQuota(connectionId: string): void {
+ if (!connectionId) return;
+ try {
+ getDbInstance()
+ .prepare("DELETE FROM provider_quota_state WHERE connection_id = ?")
+ .run(connectionId);
+ } catch {
+ // best-effort
+ }
+}
diff --git a/src/lib/quota/quotaAdapters.ts b/src/lib/quota/quotaAdapters.ts
new file mode 100644
index 0000000000..1bdb636451
--- /dev/null
+++ b/src/lib/quota/quotaAdapters.ts
@@ -0,0 +1,128 @@
+/**
+ * quotaAdapters.ts — Provider-specific quota header adapters.
+ *
+ * Extracts and normalizes rate limit & token budget headers from provider HTTP responses
+ * (OpenAI, Anthropic, Gemini, OpenRouter, ModelScope, Generic) into standardized
+ * provider quota states.
+ *
+ * Part of: Quota-aware provider scheduling (Phase 2).
+ */
+
+import { recordProviderQuotaUsage, getProviderQuota } from "./providerQuotaState";
+
+export interface ParsedQuotaHeaderResult {
+ tokensUsed?: number;
+ tokenLimit?: number;
+ tokensRemaining?: number;
+ windowResetMs?: number;
+}
+
+/**
+ * Parse rate-limit headers from an HTTP Response or Headers object into a normalized quota result.
+ */
+export function parseProviderQuotaHeaders(
+ headers: Headers | Record,
+ provider?: string
+): ParsedQuotaHeaderResult | null {
+ if (!headers) return null;
+
+ const getHeader = (name: string): string | null => {
+ if (typeof (headers as Headers).get === "function") {
+ return (headers as Headers).get(name);
+ }
+ const record = headers as Record;
+ const val = record[name] ?? record[name.toLowerCase()] ?? record[name.toUpperCase()];
+ if (Array.isArray(val)) return val[0] ?? null;
+ return val ?? null;
+ };
+
+ const parseNum = (val: string | null): number | undefined => {
+ if (!val) return undefined;
+ const cleaned = val.replace(/[^0-9.]/g, "");
+ const num = parseFloat(cleaned);
+ return isNaN(num) ? undefined : num;
+ };
+
+ const parseResetMs = (val: string | null): number | undefined => {
+ if (!val) return undefined;
+ const now = Date.now();
+ // Check if ISO date string
+ if (val.includes("T") || val.includes("Z")) {
+ const parsed = Date.parse(val);
+ if (!isNaN(parsed)) return Math.max(0, parsed - now);
+ }
+ // Check if seconds / ms string (e.g. "60s", "100ms", "0.5s", or raw number)
+ if (val.endsWith("ms")) return parseNum(val);
+ if (val.endsWith("s")) return (parseNum(val) ?? 0) * 1000;
+ if (val.endsWith("m")) return (parseNum(val) ?? 0) * 60 * 1000;
+ if (val.endsWith("h")) return (parseNum(val) ?? 0) * 3600 * 1000;
+
+ const rawNum = parseNum(val);
+ if (rawNum !== undefined) {
+ // If epoch timestamp (> 1e9), convert to remaining ms
+ if (rawNum > 1_000_000_000) {
+ return Math.max(0, rawNum * 1000 - now);
+ }
+ return rawNum * 1000; // assume relative seconds
+ }
+ return undefined;
+ };
+
+ const prov = (provider || "").toLowerCase();
+
+ // 1. Anthropic Headers
+ if (prov === "anthropic" || getHeader("anthropic-ratelimit-input-tokens-limit")) {
+ const limit = parseNum(getHeader("anthropic-ratelimit-input-tokens-limit"));
+ const remaining = parseNum(getHeader("anthropic-ratelimit-input-tokens-remaining"));
+ const resetStr = getHeader("anthropic-ratelimit-input-tokens-reset");
+ const windowResetMs = parseResetMs(resetStr);
+
+ if (limit !== undefined || remaining !== undefined) {
+ const tokensUsed = limit !== undefined && remaining !== undefined ? Math.max(0, limit - remaining) : undefined;
+ return { tokenLimit: limit, tokensRemaining: remaining, tokensUsed, windowResetMs };
+ }
+ }
+
+ // 2. OpenAI / Standard x-ratelimit-*
+ const limitTokens = parseNum(getHeader("x-ratelimit-limit-tokens"));
+ const remainingTokens = parseNum(getHeader("x-ratelimit-remaining-tokens"));
+ const resetTokens = getHeader("x-ratelimit-reset-tokens");
+ if (limitTokens !== undefined || remainingTokens !== undefined) {
+ const tokensUsed = limitTokens !== undefined && remainingTokens !== undefined ? Math.max(0, limitTokens - remainingTokens) : undefined;
+ return { tokenLimit: limitTokens, tokensRemaining: remainingTokens, tokensUsed, windowResetMs: parseResetMs(resetTokens) };
+ }
+
+ // 3. OpenRouter / Generic Request level headers
+ const genericLimit = parseNum(getHeader("x-ratelimit-limit"));
+ const genericRemaining = parseNum(getHeader("x-ratelimit-remaining"));
+ const genericReset = getHeader("x-ratelimit-reset");
+ if (genericLimit !== undefined || genericRemaining !== undefined) {
+ const tokensUsed = genericLimit !== undefined && genericRemaining !== undefined ? Math.max(0, genericLimit - genericRemaining) : undefined;
+ return { tokenLimit: genericLimit, tokensRemaining: genericRemaining, tokensUsed, windowResetMs: parseResetMs(genericReset) };
+ }
+
+ return null;
+}
+
+/**
+ * Apply parsed quota headers directly to the provider quota state ledger.
+ */
+export function applyQuotaHeadersToState(
+ connectionId: string,
+ model: string,
+ headers: Headers | Record,
+ provider?: string
+): void {
+ const parsed = parseProviderQuotaHeaders(headers, provider);
+ if (!parsed || (!parsed.tokensUsed && !parsed.tokenLimit)) return;
+
+ const now = Date.now();
+ const windowReset = parsed.windowResetMs ? now + parsed.windowResetMs : now + 60_000;
+ const tokenLimit = parsed.tokenLimit ?? 0;
+ const tokensUsed = parsed.tokensUsed ?? (parsed.tokenLimit && parsed.tokensRemaining ? parsed.tokenLimit - parsed.tokensRemaining : 0);
+
+ recordProviderQuotaUsage(connectionId, model, tokensUsed, {
+ tokenLimit,
+ windowMs: Math.max(0, windowReset - now),
+ });
+}
diff --git a/src/lib/quota/quotaAnalytics.ts b/src/lib/quota/quotaAnalytics.ts
new file mode 100644
index 0000000000..fca1f11b1b
--- /dev/null
+++ b/src/lib/quota/quotaAnalytics.ts
@@ -0,0 +1,114 @@
+/**
+ * quotaAnalytics.ts — Usage analytics for provider quota state & remaining capacity.
+ *
+ * Computes aggregated capacity metrics, average remaining ratios, exhausted connection counts,
+ * and per-connection quota usage summaries for dashboard visibility.
+ *
+ * Part of: Quota-aware provider scheduling (Phase 2).
+ */
+
+import { getDbInstance } from "@/lib/db/core";
+import { createLogger } from "@/shared/utils/logger";
+
+const log = createLogger("quota:analytics");
+
+export interface QuotaAnalyticsSummary {
+ totalConnectionsTracked: number;
+ exhaustedConnections: number;
+ healthyConnections: number;
+ averageRemainingRatio: number;
+ totalTokensUsed: number;
+ totalTokenLimit: number;
+ connections: Array<{
+ connectionId: string;
+ model: string;
+ tokensUsed: number;
+ tokenLimit: number;
+ tokensRemaining: number;
+ remainingRatio: number;
+ windowReset: number;
+ isExhausted: boolean;
+ }>;
+}
+
+/**
+ * Compute real-time quota analytics across all provider connections.
+ */
+export function getQuotaAnalyticsSummary(): QuotaAnalyticsSummary {
+ try {
+ const db = getDbInstance();
+ const rows = db.prepare("SELECT * FROM provider_quota_state").all() as Array<{
+ connection_id: string;
+ model: string;
+ tokens_used: number;
+ token_limit: number;
+ window_start: number;
+ window_reset: number;
+ }>;
+
+ const now = Date.now();
+ let totalTokensUsed = 0;
+ let totalTokenLimit = 0;
+ let exhaustedConnections = 0;
+ let healthyConnections = 0;
+ let ratioSum = 0;
+
+ const connections = rows.map((r) => {
+ const tokensUsed = Number(r.tokens_used ?? 0);
+ const tokenLimit = Number(r.token_limit ?? 0);
+ const windowReset = Number(r.window_reset ?? 0);
+ const isExpired = windowReset > 0 && now > windowReset;
+
+ const effectiveUsed = isExpired ? 0 : tokensUsed;
+ const effectiveLimit = isExpired ? 0 : tokenLimit;
+ const tokensRemaining = Math.max(0, effectiveLimit - effectiveUsed);
+ const remainingRatio = effectiveLimit > 0 ? tokensRemaining / effectiveLimit : 1.0;
+ const isExhausted = effectiveLimit > 0 && remainingRatio <= 0.05;
+
+ totalTokensUsed += effectiveUsed;
+ totalTokenLimit += effectiveLimit;
+ ratioSum += remainingRatio;
+
+ if (isExhausted) {
+ exhaustedConnections++;
+ } else {
+ healthyConnections++;
+ }
+
+ return {
+ connectionId: String(r.connection_id),
+ model: String(r.model),
+ tokensUsed: effectiveUsed,
+ tokenLimit: effectiveLimit,
+ tokensRemaining,
+ remainingRatio,
+ windowReset,
+ isExhausted,
+ };
+ });
+
+ const count = connections.length;
+ const averageRemainingRatio = count > 0 ? ratioSum / count : 1.0;
+
+ return {
+ totalConnectionsTracked: count,
+ exhaustedConnections,
+ healthyConnections,
+ averageRemainingRatio,
+ totalTokensUsed,
+ totalTokenLimit,
+ connections,
+ };
+ } catch (error) {
+ log.error("Failed to compute quota analytics summary", error);
+ return {
+ totalConnectionsTracked: 0,
+ exhaustedConnections: 0,
+ healthyConnections: 0,
+ averageRemainingRatio: 1.0,
+ totalTokensUsed: 0,
+ totalTokenLimit: 0,
+ connections: [],
+ };
+ }
+}
diff --git a/src/lib/quota/quotaResetTimers.ts b/src/lib/quota/quotaResetTimers.ts
new file mode 100644
index 0000000000..875be7959b
--- /dev/null
+++ b/src/lib/quota/quotaResetTimers.ts
@@ -0,0 +1,69 @@
+/**
+ * quotaResetTimers.ts — Automated quota window reset timers and capacity recovery.
+ *
+ * Tracks window reset timestamps and automatically clears exhausted provider quota
+ * states when their reset windows elapse.
+ *
+ * Part of: Quota-aware provider scheduling (Phase 2).
+ */
+
+import { getDbInstance } from "@/lib/db/core";
+import { createLogger } from "@/shared/utils/logger";
+
+const log = createLogger("quota:reset-timers");
+
+export interface QuotaResetItem {
+ connectionId: string;
+ model: string;
+ tokensUsed: number;
+ tokenLimit: number;
+ windowReset: number;
+ timeRemainingMs: number;
+}
+
+/**
+ * Get all active quota states and their remaining window reset times.
+ */
+export function getActiveQuotaResetItems(): QuotaResetItem[] {
+ try {
+ const db = getDbInstance();
+ const rows = db.prepare("SELECT * FROM provider_quota_state WHERE window_reset > 0").all() as Array<{
+ connection_id: string;
+ model: string;
+ tokens_used: number;
+ token_limit: number;
+ window_reset: number;
+ }>;
+
+ const now = Date.now();
+ return rows.map((r) => ({
+ connectionId: String(r.connection_id),
+ model: String(r.model),
+ tokensUsed: Number(r.tokens_used),
+ tokenLimit: Number(r.token_limit),
+ windowReset: Number(r.window_reset),
+ timeRemainingMs: Math.max(0, Number(r.window_reset) - now),
+ }));
+ } catch (error) {
+ log.error("Failed to query active quota reset items", error);
+ return [];
+ }
+}
+
+/**
+ * Purge or reset all expired quota windows in SQLite.
+ * Returns the count of reset connections.
+ */
+export function resetExpiredQuotaWindows(): number {
+ try {
+ const db = getDbInstance();
+ const now = Date.now();
+ const result = db
+ .prepare("DELETE FROM provider_quota_state WHERE window_reset > 0 AND window_reset <= ?")
+ .run(now);
+ return result.changes ?? 0;
+ } catch (error) {
+ log.error("Failed to reset expired quota windows", error);
+ return 0;
+ }
+}
diff --git a/src/lib/quota/quotaScheduler.ts b/src/lib/quota/quotaScheduler.ts
new file mode 100644
index 0000000000..38d0301e1f
--- /dev/null
+++ b/src/lib/quota/quotaScheduler.ts
@@ -0,0 +1,102 @@
+/**
+ * quotaScheduler.ts — pre-request capacity decision.
+ *
+ * Combines the per-connection token budget ledger (providerQuotaState) with
+ * the request cost estimate (tokenEstimator) to answer one question:
+ *
+ * "Can this connection afford this request without exceeding its
+ * configured per-window token budget?"
+ *
+ * The scheduler NEVER throws and NEVER blocks the request path when quota
+ * tracking is unconfigured — it fails open (returns `affordable: true`),
+ * preserving existing routing behavior. When a budget IS configured and the
+ * estimated cost exceeds the remaining budget, it returns
+ * `affordable: false` with the reason, and the caller should prefer another
+ * connection (the same failover machinery used for 429s).
+ *
+ * Part of: Quota-aware provider scheduling (feat/quota-aware-scheduling).
+ */
+import { getProviderQuota, recordProviderQuotaUsage } from "./providerQuotaState";
+import { estimateChatTokenCost } from "./tokenEstimator";
+
+export interface QuotaDecision {
+ affordable: boolean;
+ reason?: "exhausted" | "insufficient_budget" | "unconfigured";
+ /** remaining tokens in the window when known */
+ tokensRemaining?: number;
+ /** estimated cost of this request */
+ estimatedCost?: number;
+ /** 0..1 remaining ratio when known (1 when unknown) */
+ remainingRatio: number;
+}
+
+/**
+ * Decide whether (connectionId, model) can afford a request.
+ *
+ * @param connectionId provider connection id
+ * @param model model string (as routed)
+ * @param requestBody parsed chat body (used for the cost estimate)
+ * @returns a decision — always resolves, never throws
+ */
+export function canAffordRequest(
+ connectionId: string,
+ model: string,
+ requestBody: Record | null | undefined
+): QuotaDecision {
+ if (!connectionId || !model) {
+ return { affordable: true, reason: "unconfigured", remainingRatio: 1 };
+ }
+
+ const snapshot = getProviderQuota(connectionId, model);
+ if (!snapshot || !snapshot.known || snapshot.tokenLimit <= 0) {
+ // No configured budget → nothing to enforce → affordable.
+ return { affordable: true, reason: "unconfigured", remainingRatio: 1 };
+ }
+
+ const cost = estimateChatTokenCost(requestBody);
+ const remaining = snapshot.tokensRemaining;
+ const remainingRatio = snapshot.remainingRatio;
+
+ if (remaining <= 0) {
+ return {
+ affordable: false,
+ reason: "exhausted",
+ tokensRemaining: 0,
+ estimatedCost: cost.totalTokens,
+ remainingRatio: 0,
+ };
+ }
+
+ if (cost.totalTokens > remaining) {
+ return {
+ affordable: false,
+ reason: "insufficient_budget",
+ tokensRemaining: remaining,
+ estimatedCost: cost.totalTokens,
+ remainingRatio,
+ };
+ }
+
+ return {
+ affordable: true,
+ tokensRemaining: remaining,
+ estimatedCost: cost.totalTokens,
+ remainingRatio,
+ };
+}
+
+/**
+ * Reserve budget for a request (call AFTER a successful dispatch decision,
+ * before/around the upstream call). Best-effort: never throws.
+ */
+export function reserveQuota(
+ connectionId: string,
+ model: string,
+ requestBody: Record | null | undefined,
+ opts: { tokenLimit?: number; windowMs?: number } = {}
+): void {
+ if (!connectionId || !model) return;
+ const cost = estimateChatTokenCost(requestBody);
+ if (cost.totalTokens <= 0) return;
+ recordProviderQuotaUsage(connectionId, model, cost.totalTokens, opts);
+}
diff --git a/src/lib/quota/tokenEstimator.ts b/src/lib/quota/tokenEstimator.ts
new file mode 100644
index 0000000000..5e254ba1bf
--- /dev/null
+++ b/src/lib/quota/tokenEstimator.ts
@@ -0,0 +1,97 @@
+/**
+ * tokenEstimator.ts — cheap, deterministic request token-cost estimation.
+ *
+ * Estimates the token cost of a chat request (input + reserved output) so
+ * the quota scheduler can decide whether a connection has budget before
+ * dispatching. Not a model — a heuristic:
+ * - chars / 4 approximates tokens for most latin text (OpenAI's classic
+ * heuristic); CJK and code skew higher, so the estimate is a floor.
+ * - max_tokens / max_completion_tokens reserves the output budget when
+ * present; otherwise a small default output allowance is used.
+ *
+ * The estimate deliberately OVER-provisions input (×1.1) so an exhausted
+ * budget is not misjudged as available. Errors never throw — a broken
+ * estimate degrades to "unknown cost" (scheduler treats as affordable).
+ */
+
+export interface TokenCostEstimate {
+ /** estimated input tokens (may be 0 when body is unparseable) */
+ inputTokens: number;
+ /** reserved output budget (max_tokens or default) */
+ outputTokens: number;
+ /** input + output */
+ totalTokens: number;
+}
+
+const DEFAULT_OUTPUT_ALLOWANCE = 1024;
+const CHARS_PER_TOKEN = 4;
+const OVER_PROVISION = 1.1;
+
+/** Count a string's tokens by chars/4 (floor). */
+export function estimateStringTokens(text: string): number {
+ if (!text) return 0;
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
+}
+
+/**
+ * Estimate the token cost of an OpenAI-style chat body.
+ * Accepts both `messages` (chat.completions) and `input` (Responses API).
+ */
+export function estimateChatTokenCost(
+ body: Record | null | undefined
+): TokenCostEstimate {
+ if (!body || typeof body !== "object") {
+ return {
+ inputTokens: 0,
+ outputTokens: DEFAULT_OUTPUT_ALLOWANCE,
+ totalTokens: DEFAULT_OUTPUT_ALLOWANCE,
+ };
+ }
+
+ let inputTokens = 0;
+
+ const messages = body.messages;
+ if (Array.isArray(messages)) {
+ for (const msg of messages) {
+ if (!msg || typeof msg !== "object") continue;
+ const content = (msg as Record).content;
+ if (typeof content === "string") {
+ inputTokens += estimateStringTokens(content);
+ } else if (Array.isArray(content)) {
+ for (const part of content) {
+ if (part && typeof part === "object") {
+ const text = (part as Record).text;
+ if (typeof text === "string") inputTokens += estimateStringTokens(text);
+ }
+ }
+ }
+ }
+ }
+
+ const input = body.input;
+ if (Array.isArray(input)) {
+ for (const item of input) {
+ if (!item || typeof item !== "object") continue;
+ const text = (item as Record).text;
+ if (typeof text === "string") inputTokens += estimateStringTokens(text);
+ }
+ }
+
+ if (typeof body.system === "string") {
+ inputTokens += estimateStringTokens(body.system);
+ }
+
+ // Reserved output budget: max_tokens / max_completion_tokens win; fall back
+ // to the default allowance.
+ const rawMax =
+ typeof body.max_tokens === "number"
+ ? body.max_tokens
+ : typeof body.max_completion_tokens === "number"
+ ? body.max_completion_tokens
+ : undefined;
+ const outputTokens =
+ typeof rawMax === "number" && rawMax > 0 ? Math.ceil(rawMax) : DEFAULT_OUTPUT_ALLOWANCE;
+
+ const totalTokens = Math.ceil(inputTokens * OVER_PROVISION) + outputTokens;
+ return { inputTokens, outputTokens, totalTokens };
+}
diff --git a/tests/unit/check-migration-numbering.test.ts b/tests/unit/check-migration-numbering.test.ts
index 17d1416e86..0d5c152896 100644
--- a/tests/unit/check-migration-numbering.test.ts
+++ b/tests/unit/check-migration-numbering.test.ts
@@ -108,7 +108,8 @@ test("frozen allowlists match the documented legacy and stacked-series gaps", ()
assert.equal((KNOWN_GAPS as Set).has("145"), false);
// 147 left the gap list when 147_api_keys_model_access_mode.sql landed (same pattern as 143).
assert.equal((KNOWN_GAPS as Set).has("147"), false);
- assert.ok((KNOWN_GAPS as Set).has("148"));
+ // 148 left the gap list when 148_provider_quota_state.sql landed on this branch (same pattern as 143/147).
+ assert.equal((KNOWN_GAPS as Set).has("148"), false);
// 149 left the gap list when 149_api_key_combo_access.sql landed (#10066).
assert.equal((KNOWN_GAPS as Set).has("149"), false);
// "041" was removed from KNOWN_DUPLICATE_VERSIONS in 6A.3 (stale: no physical
diff --git a/tests/unit/quota-phase2.test.ts b/tests/unit/quota-phase2.test.ts
new file mode 100644
index 0000000000..64e0600e2a
--- /dev/null
+++ b/tests/unit/quota-phase2.test.ts
@@ -0,0 +1,111 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-quota-phase2-"));
+process.env.DATA_DIR = TEST_DATA_DIR;
+
+const coreDb = await import("../../src/lib/db/core.ts");
+const { parseProviderQuotaHeaders, applyQuotaHeadersToState } = await import(
+ "../../src/lib/quota/quotaAdapters"
+);
+const { getQuotaAnalyticsSummary } = await import("../../src/lib/quota/quotaAnalytics");
+const { getActiveQuotaResetItems, resetExpiredQuotaWindows } = await import(
+ "../../src/lib/quota/quotaResetTimers"
+);
+const { recordProviderQuotaUsage, getProviderQuota } = await import(
+ "../../src/lib/quota/providerQuotaState"
+);
+const { getDbInstance } = coreDb;
+
+async function resetStorage() {
+ coreDb.resetDbInstance();
+ fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
+ fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
+}
+
+test.beforeEach(async () => {
+ await resetStorage();
+});
+
+test.after(() => {
+ coreDb.resetDbInstance();
+ fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
+});
+
+test("parseProviderQuotaHeaders: parses OpenAI rate limit headers", () => {
+ const headers = new Headers({
+ "x-ratelimit-limit-tokens": "100000",
+ "x-ratelimit-remaining-tokens": "80000",
+ "x-ratelimit-reset-tokens": "60s",
+ });
+ const parsed = parseProviderQuotaHeaders(headers, "openai");
+ assert.ok(parsed);
+ assert.equal(parsed?.tokenLimit, 100000);
+ assert.equal(parsed?.tokensRemaining, 80000);
+ assert.equal(parsed?.tokensUsed, 20000);
+ assert.equal(parsed?.windowResetMs, 60000);
+});
+
+test("parseProviderQuotaHeaders: parses Anthropic rate limit headers", () => {
+ const headers = new Headers({
+ "anthropic-ratelimit-input-tokens-limit": "50000",
+ "anthropic-ratelimit-input-tokens-remaining": "10000",
+ "anthropic-ratelimit-input-tokens-reset": "30s",
+ });
+ const parsed = parseProviderQuotaHeaders(headers, "anthropic");
+ assert.ok(parsed);
+ assert.equal(parsed?.tokenLimit, 50000);
+ assert.equal(parsed?.tokensRemaining, 10000);
+ assert.equal(parsed?.tokensUsed, 40000);
+ assert.equal(parsed?.windowResetMs, 30000);
+});
+
+test("applyQuotaHeadersToState & getQuotaAnalyticsSummary: records and aggregates quota analytics", () => {
+ const connId = "test-conn-p2-01";
+ const model = "gpt-4o";
+ const headers = {
+ "x-ratelimit-limit-tokens": "100000",
+ "x-ratelimit-remaining-tokens": "20000",
+ "x-ratelimit-reset-tokens": "120s",
+ };
+
+ applyQuotaHeadersToState(connId, model, headers, "openai");
+
+ const snapshot = getProviderQuota(connId, model);
+ assert.ok(snapshot);
+ assert.equal(snapshot?.tokensUsed, 80000);
+ assert.equal(snapshot?.tokenLimit, 100000);
+
+ const analytics = getQuotaAnalyticsSummary();
+ assert.ok(analytics.totalConnectionsTracked > 0);
+ assert.ok(analytics.connections.some((c) => c.connectionId === connId));
+});
+
+test("quotaResetTimers: tracks active reset items and purges expired windows", () => {
+ const connId = "test-conn-expired";
+ const model = "claude-sonnet-4-6";
+ const now = Date.now();
+
+ // Seed an already-expired window directly (recordProviderQuotaUsage always
+ // computes windows from Date.now(), so it cannot create a past window).
+ const db = getDbInstance();
+ db.prepare(
+ `INSERT OR REPLACE INTO provider_quota_state
+ (connection_id, model, tokens_used, token_limit, window_start, window_reset, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
+ ).run(
+ connId,
+ model,
+ 5000,
+ 5000,
+ now - 10_000,
+ now - 1_000,
+ new Date().toISOString()
+ );
+
+ const expiredCount = resetExpiredQuotaWindows();
+ assert.ok(expiredCount >= 1);
+});
diff --git a/tests/unit/quota-scheduler.test.ts b/tests/unit/quota-scheduler.test.ts
new file mode 100644
index 0000000000..d105945a0a
--- /dev/null
+++ b/tests/unit/quota-scheduler.test.ts
@@ -0,0 +1,89 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-quota-sched-"));
+process.env.DATA_DIR = TEST_DATA_DIR;
+
+const coreDb = await import("../../src/lib/db/core.ts");
+const { canAffordRequest } = await import("../../src/lib/quota/quotaScheduler");
+const { clearProviderQuota, getProviderQuota, recordProviderQuotaUsage } =
+ await import("../../src/lib/quota/providerQuotaState");
+
+async function resetStorage() {
+ coreDb.resetDbInstance();
+ fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
+ fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
+}
+
+test.beforeEach(async () => {
+ await resetStorage();
+});
+
+test.after(() => {
+ coreDb.resetDbInstance();
+ fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
+});
+
+const CONN = "test-conn-quota";
+const MODEL = "test-model";
+
+test("canAffordRequest: fails open when no budget configured", () => {
+ clearProviderQuota(CONN);
+ const decision = canAffordRequest(CONN, MODEL, { messages: [{ role: "user", content: "hi" }] });
+ assert.equal(decision.affordable, true);
+ assert.equal(decision.reason, "unconfigured");
+});
+
+test("canAffordRequest: skips exhausted budget", () => {
+ clearProviderQuota(CONN);
+ recordProviderQuotaUsage(CONN, MODEL, 10_000, { tokenLimit: 10_000, windowMs: 60_000 });
+ const decision = canAffordRequest(CONN, MODEL, {
+ messages: [{ role: "user", content: "hello world this is a request" }],
+ });
+ assert.equal(decision.affordable, false);
+ assert.equal(decision.reason, "exhausted");
+});
+
+test("canAffordRequest: blocks when cost exceeds remaining", () => {
+ clearProviderQuota(CONN);
+ recordProviderQuotaUsage(CONN, MODEL, 9_000, { tokenLimit: 10_000, windowMs: 60_000 });
+ const decision = canAffordRequest(CONN, MODEL, {
+ messages: [{ role: "user", content: "x".repeat(4 * 800) }], // ~800 tokens
+ max_tokens: 2000,
+ });
+ assert.equal(decision.affordable, false);
+ assert.equal(decision.reason, "insufficient_budget");
+});
+
+test("canAffordRequest: allows within budget", () => {
+ clearProviderQuota(CONN);
+ recordProviderQuotaUsage(CONN, MODEL, 1_000, { tokenLimit: 10_000, windowMs: 60_000 });
+ const decision = canAffordRequest(CONN, MODEL, { messages: [{ role: "user", content: "hi" }] });
+ assert.equal(decision.affordable, true);
+ assert.ok((decision.tokensRemaining ?? 0) > 0);
+});
+
+test("recordProviderQuotaUsage: seeds a row and accumulates", () => {
+ clearProviderQuota(CONN);
+ recordProviderQuotaUsage(CONN, MODEL, 100, { tokenLimit: 1_000, windowMs: 60_000 });
+ recordProviderQuotaUsage(CONN, MODEL, 150, { tokenLimit: 1_000, windowMs: 60_000 });
+ const snap = getProviderQuota(CONN, MODEL);
+ assert.ok(snap);
+ assert.equal(snap.tokensUsed, 250);
+ assert.equal(snap.tokenLimit, 1_000);
+ assert.equal(snap.tokensRemaining, 750);
+});
+
+test("getProviderQuota: returns null for unknown pair", () => {
+ clearProviderQuota(CONN);
+ assert.equal(getProviderQuota(CONN, "nope-model"), null);
+});
+
+test("getProviderQuota: ignores non-positive deltas", () => {
+ clearProviderQuota(CONN);
+ recordProviderQuotaUsage(CONN, MODEL, 0, { tokenLimit: 1_000 });
+ assert.equal(getProviderQuota(CONN, MODEL), null);
+});
diff --git a/tests/unit/quota-token-estimator.test.ts b/tests/unit/quota-token-estimator.test.ts
new file mode 100644
index 0000000000..2a15cb49d5
--- /dev/null
+++ b/tests/unit/quota-token-estimator.test.ts
@@ -0,0 +1,83 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+
+import { estimateChatTokenCost, estimateStringTokens } from "../../src/lib/quota/tokenEstimator";
+
+test("estimateStringTokens: chars/4 heuristic", () => {
+ assert.equal(estimateStringTokens(""), 0);
+ assert.equal(estimateStringTokens("abcd"), 1);
+ assert.equal(estimateStringTokens("abcdefgh"), 2);
+});
+
+test("estimateChatTokenCost: sums message content strings", () => {
+ const cost = estimateChatTokenCost({
+ messages: [
+ { role: "user", content: "Hello world, this is a test message" },
+ { role: "assistant", content: "A shorter reply" },
+ ],
+ });
+ assert.ok(cost.inputTokens > 0);
+ // total = input × 1.1 (over-provision) + output budget
+ assert.equal(cost.totalTokens, Math.ceil(cost.inputTokens * 1.1) + cost.outputTokens);
+});
+
+test("estimateChatTokenCost: includes system prompt", () => {
+ const withoutSystem = estimateChatTokenCost({
+ messages: [{ role: "user", content: "hi there" }],
+ });
+ const withSystem = estimateChatTokenCost({
+ system: "You are a helpful assistant with a fairly long system prompt to count",
+ messages: [{ role: "user", content: "hi there" }],
+ });
+ assert.ok(withSystem.inputTokens > withoutSystem.inputTokens);
+});
+
+test("estimateChatTokenCost: honors max_tokens as output budget", () => {
+ const cost = estimateChatTokenCost({
+ messages: [{ role: "user", content: "hi" }],
+ max_tokens: 2000,
+ });
+ assert.equal(cost.outputTokens, 2000);
+});
+
+test("estimateChatTokenCost: honors max_completion_tokens (Responses API)", () => {
+ const cost = estimateChatTokenCost({
+ messages: [{ role: "user", content: "hi" }],
+ max_completion_tokens: 500,
+ });
+ assert.equal(cost.outputTokens, 500);
+});
+
+test("estimateChatTokenCost: defaults output allowance when unset", () => {
+ const cost = estimateChatTokenCost({ messages: [{ role: "user", content: "hi" }] });
+ assert.equal(cost.outputTokens, 1024);
+});
+
+test("estimateChatTokenCost: handles multimodal content arrays", () => {
+ const cost = estimateChatTokenCost({
+ messages: [
+ {
+ role: "user",
+ content: [
+ { type: "text", text: "Describe this image" },
+ { type: "image_url", image_url: { url: "data:image/png;base64,xxx" } },
+ ],
+ },
+ ],
+ });
+ assert.ok(cost.inputTokens > 0);
+});
+
+test("estimateChatTokenCost: handles Responses API input array", () => {
+ const cost = estimateChatTokenCost({
+ input: [{ role: "user", text: "What is the capital of France" }],
+ });
+ assert.ok(cost.inputTokens > 0);
+});
+
+test("estimateChatTokenCost: never throws on malformed bodies", () => {
+ for (const bad of [null, undefined, {}, { messages: "nope" }, { messages: [null, 42] }]) {
+ const cost = estimateChatTokenCost(bad as Record);
+ assert.ok(cost.totalTokens >= 0);
+ }
+});