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 RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 117 domain modules, 150 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 117 domain modules, 151 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (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); + } +});