Merge remote-tracking branch 'origin/release/v3.8.51' into feat/orch-fase3-d

This commit is contained in:
diegosouzapw
2026-09-10 08:42:29 -03:00
112 changed files with 5883 additions and 1945 deletions

View File

@@ -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 (169 migrations) |
| Database | `src/lib/db/` | SQLite domain modules (171 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 110 tools (45 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 |

View File

@@ -1244,7 +1244,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>&gt;=22.22.2 &lt;23 || &gt;=24.0.0 &lt;27</code></td></tr>
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 169 migrations</td></tr>
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 171 migrations</td></tr>
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>

View File

@@ -0,0 +1 @@
- **fix(quota):** share one upstream quota read when identical saturation checks arrive at the same time, so traffic bursts don't multiply provider API calls ([#12787](https://github.com/diegosouzapw/OmniRoute/pull/12787)) — thanks @maxmad64bis

View File

@@ -0,0 +1 @@
- **fix(db):** ignore expired or invalid rate-limit cooldown writes so a stale timestamp can't lock a connection that should be usable — clearing still works as before ([#12788](https://github.com/diegosouzapw/OmniRoute/pull/12788)) — thanks @maxmad64bis

View File

@@ -0,0 +1 @@
- **fix(db):** provider stats stay truthful on empty and legacy databases: fallback counts default to `0` instead of `null`, latency averages read `null` (not `0`) when no durations were recorded, failures logged before the error-type column existed group under `pre_migration` instead of `unclassified`, and per-provider queries use two new composite indexes ([#12832](https://github.com/diegosouzapw/OmniRoute/pull/12832)) — thanks @maxmad64bis

View File

@@ -0,0 +1 @@
- **fix(db):** WAL maintenance lives in its own module: a `TRUNCATE` checkpoint that hits a busy database now warns with its streak and retries once via `PASSIVE` instead of logging success, and closing-time checkpoints no longer report success on builds without a database file, with the busy totals visible in the authenticated monitoring health payload ([#12853](https://github.com/diegosouzapw/OmniRoute/pull/12853))

View File

@@ -0,0 +1 @@
- **fix(health):** quota with no snapshots reads empty instead of a contradicting 0% ([#12857](https://github.com/diegosouzapw/OmniRoute/pull/12857)) — thanks @maxmad64bis

View File

@@ -0,0 +1 @@
- **fix(codex):** dashboard "clear cooldown" and the CAS recovery path now drop nested `codexScopeRateLimitedUntil` maps in the same write that nulls `rate_limited_until`, and fresh quota snapshots with headroom lift fallback-sourced scope cooldowns parked by quota preflight ([#12817](https://github.com/diegosouzapw/OmniRoute/issues/12817), [#12860](https://github.com/diegosouzapw/OmniRoute/issues/12860), [#12951](https://github.com/diegosouzapw/OmniRoute/pull/12951)) The dashboard reset-credit button (`consumeCodexResetCredit`) now exercises that same snapshot path after a successful redeem, so a filled quota bar is enough to unpark a leftover fallback Codex child without another manual clear.

View File

@@ -0,0 +1 @@
- Fix a concurrency-slot leak in the DeepSeek PoW solver: a worker that failed to spawn (for example a missing worker script) never released its slot, so `MAX_CONCURRENT_WORKERS` failures disabled the solver until restart.

View File

@@ -0,0 +1 @@
- Restore the API-route typecheck gate: the non-streaming leg result lost its discriminated-union narrowing after the server-owned tool loop reassignment, producing 13 new TS2339 diagnostics in `chatCore.ts`.

View File

@@ -0,0 +1 @@
- Restore four non-streaming behaviours the server-owned tool loop refactor dropped: client aborts map to 499 with the fixed `Request aborted` message, an aborted request no longer logs a synthetic `clientResponse`, Claude prompt-cache telemetry is recorded again, and a body that cannot be canonicalized no longer throws when the tool loop is off.

View File

@@ -0,0 +1 @@
- Redact Google API keys of any length in error bodies: the pattern required exactly 39 characters, so shorter or longer `AIza…` credentials passed through unredacted.

View File

@@ -0,0 +1 @@
- fix(volcengine): resolve quota window mapping gaps, safe multi-connection console binding, and response cookie sanitization

View File

@@ -0,0 +1 @@
- Bring the documented counts back in line with the code: 171 migrations (was 169) and 20 routing strategies (was 19), plus the regenerated `cli-tunnel` skill reference.

View File

@@ -215,11 +215,6 @@
"count": 1
}
},
"open-sse/handlers/chatCore.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 26
}
},
"open-sse/handlers/chatCore/executorHelpers.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1

View File

@@ -424,14 +424,13 @@
"open-sse/executors/codex.ts": 1505,
"open-sse/executors/cursor.ts": 1759,
"open-sse/executors/muse-spark-web.ts": 1405,
"open-sse/handlers/chatCore.ts": 5984,
"open-sse/handlers/chatCore.ts": 6021,
"open-sse/handlers/imageGeneration.ts": 3259,
"open-sse/handlers/search.ts": 1789,
"open-sse/mcp-server/schemas/tools.ts": 1621,
"open-sse/mcp-server/server.ts": 1572,
"open-sse/services/accountFallback.ts": 2467,
"open-sse/services/adobeFireflyBrowserLogin.ts": 1401,
"open-sse/services/combo.ts": 4084,
"open-sse/services/combo.ts": 4080,
"open-sse/services/combo/executeTargetAttempt.ts": 1205,
"open-sse/translator/response/openai-responses.ts": 1466,
@@ -652,5 +651,6 @@
"_rebaseline_2026_09_03_12352_apikey_acl": "PR #12352 (fix/api-key-create-acl-12275) crescimento proprio: src/lib/db/apiKeys.ts 1610->1625 (+15). A criacao de API key descartava a ACL enviada no payload; preservar essa ACL exige carregar e persistir o conjunto no mesmo chokepoint de INSERT do modulo de dominio, sem extracao possivel sem partir a funcao de criacao ao meio. Coberto pelos testes do proprio PR (54/54 focados na leva).",
"_rebaseline_2026_09_03_houminxi_combo_stacked": "Leva HouMinXi (#12624 #12626 #12632 #12637): open-sse/services/combo.ts 4075->4080 (+5), medido no tip com os quatro mergeados. Cada PR registrou o proprio crescimento contra o tip de onde forkou (o #12637 ja subira o cap para 4075); as 5 linhas restantes so aparecem quando eles empilham, porque mais de um toca o mesmo chokepoint de scoring reset-aware em combo.ts. Fiacao em ponto existente, sem extracao possivel sem partir a funcao de selecao de alvos. Coberto por combo-strategies e reset-aware-request-scope-12600 (119/119 focados na leva).",
"_rebaseline_2026_09_04_12641_continuation_effective_input": "PR #12641 crescimento proprio: src/sse/handlers/chat.ts 2450->2454 (+4). A continuacao por previous_response_id encadeava a partir de clientRawRequest.body.input, que e capturado ANTES da reconstrucao do proprio chat.ts; quando o turno anterior ja era uma continuacao, esse campo guarda so o delta do cliente, e o erro se acumulava a cada salto ate a reconstrucao virar itens de tool sem prefixo. Persistir o input EFETIVO exige as linhas no ponto onde a reconstrucao termina, dentro do fluxo de despacho. Coberto por tests/unit/responses-continuation-store.test.ts (22/22 focados na leva).",
"_rebaseline_2026_09_05_12671_combos_usage_guide_external_store": "combos/page.tsx 5018 -> 5066: #12671 replaces the effect-based localStorage read with useSyncExternalStore; the +48 lines are the store helpers (subscribe/getSnapshot/getServerSnapshot/emit) hoisted to module scope, which is the sanctioned shape and what let the react-hooks/set-state-in-effect suppression be dropped."
"_rebaseline_2026_09_05_12671_combos_usage_guide_external_store": "combos/page.tsx 5018 -> 5066: #12671 replaces the effect-based localStorage read with useSyncExternalStore; the +48 lines are the store helpers (subscribe/getSnapshot/getServerSnapshot/emit) hoisted to module scope, which is the sanctioned shape and what let the react-hooks/set-state-in-effect suppression be dropped.",
"_rebaseline_2026_09_07_chatcore_nonstreaming_regression_fixes": "Own growth: open-sse/handlers/chatCore.ts 5984->6021 (+37). Two of my own PRs on top of #12867: #12963 pins the ok variant of the non-streaming leg result in its own binding (the discriminated-union narrowing was lost across the tool-loop reassignment, 13 TS2339 under tsconfig.typecheck-api.json), and #12990 restores four behaviours the same refactor dropped — abort classification through isLocalStreamLifecycleError, the omitted synthetic clientResponse, the claudePromptCacheLogMeta rebuild on the leg path, and the lazy fail-closed fence identity. Irreducible at the existing chokepoints: each edit sits where chatCore already owns the decision, and the helpers themselves (nonStreamingProviderLeg.ts, serverOwnedToolLoopWire.ts) are under cap. Covered by tests/unit/chatcore-translation-paths.test.ts (72/74; the 2 open are issue #13043)."
}

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 356 providers, 150+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 42 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 356 providers, 150+ free providers built-in, 20 routing strategies, 12-engine token compression, a built-in MCP server with 110 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 42 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<desc>Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.</desc>
<defs>
<pattern id="gC" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/></pattern>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 356 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 356 AI providers, 150+ free tiers, about 1.47B free tokens per month, 15 to 95 percent token savings, 19 routing strategies, zero dollars to start.">
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 356 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 356 AI providers, 150+ free tiers, about 1.47B free tokens per month, 15 to 95 percent token savings, 20 routing strategies, zero dollars to start.">
<desc>Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame.</desc>
<defs>
<pattern id="gridPaperH" width="32" height="32" patternUnits="userSpaceOnUse">

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -90,7 +90,7 @@
</circle>
</g>
<text x="364" y="216" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="20" font-weight="800" fill="url(#gradBrand)">OmniRoute — Smart Router</text>
<text x="364" y="243" font-family="Consolas, 'Courier New', monospace" font-size="11.5" fill="#a1a1aa"><tspan fill="#8b5cf6">RTK + Caveman compression</tspan> · <tspan fill="#8b5cf6">19 routing strategies</tspan></text>
<text x="364" y="243" font-family="Consolas, 'Courier New', monospace" font-size="11.5" fill="#a1a1aa"><tspan fill="#8b5cf6">RTK + Caveman compression</tspan> · <tspan fill="#8b5cf6">20 routing strategies</tspan></text>
<text x="364" y="264" font-family="Consolas, 'Courier New', monospace" font-size="11.5" fill="#a1a1aa">Circuit breakers · TLS stealth · MCP · A2A · Guardrails</text>
<!-- fan-out topology (always visible) -->

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

View File

@@ -101,7 +101,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. |
| `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. |
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. |
| `OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS` | `21600000` (6h) | `src/lib/db/core.ts` | Override the periodic `wal_checkpoint(TRUNCATE)` interval (ms). Auto-checkpoint never shrinks the WAL file itself, and a long-running server never closes its DB. `0` disables. |
| `OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS` | `21600000` (6h) | `src/lib/db/walMaintenance.ts` | Override the periodic `wal_checkpoint(TRUNCATE)` interval (ms). Auto-checkpoint never shrinks the WAL file itself, and a long-running server never closes its DB. `0` disables. |
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts`, `src/lib/db/healthCheck.ts` | Set to `1` to skip the DB healthcheck entirely on startup. Useful for short-lived tasks and integration tests. |
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | Set to `1` to force the DB healthcheck loop on, even when it would normally be skipped (e.g., short-lived tasks). |
| `OMNIROUTE_SKIP_POSTINSTALL` | `0` | `scripts/postinstall.mjs` | Set to `1` to skip the native-runtime warm-up during `npm install`. Useful in CI/headless installs where sqlite is already built. |

View File

@@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <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, 169 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 171 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
@@ -124,7 +124,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ │ ├── secrets.ts # Secrets management
│ │ │ ├── stateReset.ts # State reset utilities
│ │ │ ├── migrationRunner.ts # Schema migration runner
│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ │ └── migrations/ # 171 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -389,7 +389,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 171 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -433,7 +433,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 (122 domain-specific files, 169 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 (122 domain-specific files, 171 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.

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ import { restoreNonStreamingToolNames } from "./passthroughToolNames.ts";
import { extractUsageFromResponse } from "../usageExtractor.ts";
import { sanitizeUsagePayloadForRequest } from "../../utils/usageTracking.ts";
import { createErrorResult, formatProviderError } from "../../utils/error.ts";
import { isLocalStreamLifecycleError } from "@/shared/utils/circuitBreaker";
import { unwrapClinepassEnvelope } from "../../utils/clinepassEnvelope.ts";
import { unwrapClineNonStreamingEnvelope } from "./clineResponseEnvelope.ts";
import {
@@ -232,7 +233,6 @@ function parseRetryAfterMs(response: Response): number | null {
return null;
}
function finishOk(
input: ProviderLegInput,
params: {
@@ -462,14 +462,22 @@ export async function runNonStreamingProviderLeg(
) {
throw error;
}
const failureStatus =
error instanceof Error && error.name === "AbortError"
? 499
: error instanceof Error && error.name === "TimeoutError"
? 504
: 502;
const failureMessage =
error instanceof Error
// `abort(reason)` can reject with a raw string that has no `name`/`status`, so
// `error.name === "AbortError"` is too narrow — that shape fell through to the 502
// provider-failure default (#7907). chatCore classified this through
// isLocalStreamLifecycleError before this leg took over the first send; mirror it.
const isRequestAborted = isLocalStreamLifecycleError(error);
const failureStatus = isRequestAborted
? 499
: error instanceof Error && error.name === "TimeoutError"
? 504
: 502;
// A client abort is not a provider failure: formatProviderError would stamp the raw
// upstream text as `[499]: <reason>`, leaking it to the client. chatCore has always
// normalized this to the fixed "Request aborted".
const failureMessage = isRequestAborted
? "Request aborted"
: error instanceof Error
? formatProviderError(error, provider, currentModel, failureStatus)
: "Provider request failed";
const receipt = buildReceipt(input, {

View File

@@ -106,7 +106,18 @@ function solveInWorker(
activeWorkerCount += 1;
return new Promise<number>((resolve, reject) => {
const worker = new Worker(resolveWorkerPath(), { workerData: validated });
// The slot is taken before this executor runs, so anything that throws here
// -- a missing worker script, a spawn failure -- has to hand it back. Without
// this, MAX_CONCURRENT_WORKERS spawn failures wedge the solver permanently
// and every later call reports "capacity reached" instead of the real cause.
let worker: Worker;
try {
worker = new Worker(resolveWorkerPath(), { workerData: validated });
} catch (error) {
activeWorkerCount = Math.max(0, activeWorkerCount - 1);
reject(error instanceof Error ? error : new Error(String(error)));
return;
}
let settled = false;
const cleanup = () => {

View File

@@ -56,7 +56,9 @@ export async function persistCodexChildQuotaResponse(params: {
rateLimitedUntil,
rateLimitSource: exhaustedWindow ? ("quota_reset" as const) : ("fallback" as const),
}
: {}),
: params.status === 200
? { rateLimitedUntil: null }
: {}),
});
if (!providerSpecificData) return null;

View File

@@ -330,6 +330,19 @@ function antigravityWeeklyWindowMatchesFamily(
return family === "gemini" ? key === "gemini_weekly" : key === "claude_gpt_weekly";
}
const TIME_WINDOW_KEYS = new Set([
"session",
"weekly",
"daily",
"monthly",
"session (5h)",
"weekly (7d)",
"AFPFiveHour",
"AFPWeekly",
"AFPDaily",
"AFPMonthly",
]);
function normalizeQuotaWindows(
windows: Record<string, { percentUsed: number; resetAt: string | null }>,
context: UsageToQuotaContext
@@ -340,12 +353,14 @@ function normalizeQuotaWindows(
? getAntigravityQuotaFamily(context.requestedModel)
: null;
// Claude-style explicit time windows.
if (windows["session (5h)"] && !normalized.window5h) {
normalized.window5h = windows["session (5h)"];
// Explicit time windows (canonical and legacy aliases).
const fiveHourWindow = windows["session (5h)"] || windows["session"];
if (fiveHourWindow && !normalized.window5h) {
normalized.window5h = fiveHourWindow;
}
if (windows["weekly (7d)"] && !normalized.window7d) {
normalized.window7d = windows["weekly (7d)"];
const sevenDayWindow = windows["weekly (7d)"] || windows["weekly"];
if (sevenDayWindow && !normalized.window7d) {
normalized.window7d = sevenDayWindow;
}
// Antigravity-style per-model windows: pick worst only inside requested family.
@@ -356,6 +371,7 @@ function normalizeQuotaWindows(
!key.startsWith("window") &&
!key.includes("(5h)") &&
!key.includes("(7d)") &&
!TIME_WINDOW_KEYS.has(key) &&
(requestedFamily === null ||
requestedFamily === "other" ||
getAntigravityQuotaFamily(key) === requestedFamily)

View File

@@ -101,9 +101,16 @@ function tsToIso(seconds: number): string | null {
return Number.isNaN(d.getTime()) ? null : d.toISOString();
}
const CODING_LEVEL_TO_CANONICAL: Record<string, string> = {
session: "session (5h)",
weekly: "weekly (7d)",
daily: "daily",
monthly: "monthly",
};
const CODING_WINDOW_LABEL: Record<string, string> = {
session: "Session (5h)",
weekly: "Weekly",
"session (5h)": "Session (5h)",
"weekly (7d)": "Weekly",
monthly: "Monthly",
daily: "Daily",
};
@@ -119,27 +126,29 @@ function mapCodingPlanUsage(result: JsonRecord): Record<string, UsageQuota> {
const w = toRecord(raw);
const level = String(w.Level || "").toLowerCase();
if (!level) continue;
const cap = toNumber(w.Cap, 100) || 100;
const canonicalKey = CODING_LEVEL_TO_CANONICAL[level] || level;
const rawCap = toNumber(w.Cap, 100);
const cap = rawCap > 0 ? rawCap : 100;
const usedPercent = toNumber(w.Percent, 0);
const remainingPercentage = Math.max(0, Math.min(100, cap - usedPercent));
quotas[level] = {
quotas[canonicalKey] = {
used: usedPercent,
total: cap,
remaining: Math.max(0, cap - usedPercent),
remainingPercentage,
resetAt: tsToIso(toNumber(w.ResetTimestamp, 0)),
unlimited: false,
displayName: CODING_WINDOW_LABEL[level] || level,
displayName: CODING_WINDOW_LABEL[canonicalKey] || level,
};
}
return quotas;
}
const AGENT_WINDOW_LABEL: Array<[string, string]> = [
["AFPFiveHour", "Session (5h)"],
["AFPDaily", "Daily"],
["AFPWeekly", "Weekly"],
["AFPMonthly", "Monthly"],
const AGENT_WINDOW_MAPPING: Array<[string, string, string]> = [
["AFPFiveHour", "session (5h)", "Session (5h)"],
["AFPDaily", "daily", "Daily"],
["AFPWeekly", "weekly (7d)", "Weekly"],
["AFPMonthly", "monthly", "Monthly"],
];
/**
@@ -148,16 +157,16 @@ const AGENT_WINDOW_LABEL: Array<[string, string]> = [
*/
function mapAgentPlanUsage(result: JsonRecord): Record<string, UsageQuota> {
const quotas: Record<string, UsageQuota> = {};
for (const [key, label] of AGENT_WINDOW_LABEL) {
const w = toRecord(result[key]);
for (const [sourceKey, canonicalKey, label] of AGENT_WINDOW_MAPPING) {
const w = toRecord(result[sourceKey]);
if (Object.keys(w).length === 0) continue;
const total = toNumber(w.Quota, 0);
const used = toNumber(w.Used, 0);
const remaining = Math.max(0, total - used);
const remainingPercentage =
total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 100;
total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 0;
const resetMs = toNumber(w.ResetTime, 0);
quotas[key] = {
quotas[canonicalKey] = {
used,
total,
remaining,
@@ -315,3 +324,8 @@ export async function getVolcenginePlanUsage(
};
}
}
export const __testing = {
mapCodingPlanUsage,
mapAgentPlanUsage,
};

View File

@@ -18,7 +18,13 @@ export const CREDENTIAL_PATTERNS: CredentialPattern[] = [
regex: /sk-ant-[A-Za-z0-9_-]{20,}/g,
replacement: "[REDACTED:anthropic]",
},
{ name: "google", regex: /AIza[0-9A-Za-z_-]{35}/g, replacement: "[REDACTED:google]" },
// {20,} rather than the exact {35} of a standard 39-char Google API key. #12506 added
// this pattern with the exact length; #12620 landed the anti-drift test that asserts
// /\bAIza[A-Za-z0-9_-]{20,}/ must not survive. Anything shorter or longer than 39 was
// therefore passing straight through into error bodies. In an error message
// over-redacting a string that merely starts with AIza costs nothing; under-redacting
// one leaks a credential, so the loose bound is the correct side to err on.
{ name: "google", regex: /AIza[0-9A-Za-z_-]{20,}/g, replacement: "[REDACTED:google]" },
{ name: "huggingface", regex: /hf_[A-Za-z0-9]{34}/g, replacement: "[REDACTED:hf]" },
{ name: "replicate", regex: /r8_[A-Za-z0-9]{37}/g, replacement: "[REDACTED:replicate]" },
{ name: "github", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g, replacement: "[REDACTED:github]" },

View File

@@ -277,6 +277,7 @@
"postbuild": "node scripts/build/colocate-standalone.mjs",
"release:contributors": "node scripts/release/gen-contributors.mjs",
"release:uncovered": "node scripts/release/list-uncovered-commits.mjs",
"release:reconcile": "node scripts/release/reconcile-changelog.mjs",
"test:coverage:runner": "node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,translator,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true NODE_OPTIONS=--max-old-space-size=8192 c8 --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=8 \"tests/unit/dashboard/**/*.test.ts\" && npm run test:unit:serial",
"test:unit:serial": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 \"tests/unit/serial/**/*.test.ts\"",
"alibaba:sync-allowlist": "node --import tsx/esm scripts/ops/sync-alibaba-allowlist.mjs"

View File

@@ -44,6 +44,15 @@ export const SECTIONS = Object.freeze({
const SKIP_FILES = new Set(["README.md", ".gitkeep"]);
/** The living cycle version = package.json `version` (null when unreadable → legacy first-heading mode). */
export function readVersion(root = ROOT) {
try {
return JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version || null;
} catch {
return null;
}
}
/**
* Validate one fragment's text. Returns null when OK, or a human-readable error.
* Pure — unit-tested.
@@ -89,17 +98,32 @@ export function collectFragments(root) {
/**
* Append bullets at the END of a living-section heading's bullet block (before the
* next "##"/"###" heading). Operates on the FIRST occurrence of the heading — in this
* repo's CHANGELOG the living cycle section always appears first. Pure — unit-tested.
* Throws when a needed heading is missing (the release captain adds the heading; the
* script never invents structure).
* next "##"/"###" heading). When `version` is given the heading is searched INSIDE the
* `## [version]` block only — `[Unreleased]` still carries a `### ✨ New Features`
* heading, so the first occurrence in the file is the wrong one (v3.8.51: every feature
* fragment was landing under `[Unreleased]`, #12971). Without `version` the FIRST
* occurrence is used (legacy behaviour). Pure — unit-tested. Throws when a needed heading
* is missing (the release captain adds the heading; the script never invents structure).
*/
export function insertBullets(changelogText, bulletsBySection) {
export function insertBullets(changelogText, bulletsBySection, version = null) {
let lines = changelogText.split("\n");
for (const [section, heading] of Object.entries(SECTIONS)) {
const bullets = (bulletsBySection[section] || []).map((b) => b.text ?? b);
if (bullets.length === 0) continue;
const headIdx = lines.findIndex((l) => l.trim() === heading);
let from = 0;
let to = lines.length;
if (version) {
from = lines.findIndex((l) => l.startsWith(`## [${version}]`));
if (from === -1) {
throw new Error(
`section "## [${version}]" not found in CHANGELOG.md — fragments must land in the living version section`
);
}
to = lines.findIndex((l, i) => i > from && l.startsWith("## ["));
if (to === -1) to = lines.length;
}
const rel = lines.slice(from, to).findIndex((l) => l.trim() === heading);
const headIdx = rel === -1 ? -1 : from + rel;
if (headIdx === -1) {
throw new Error(
`heading "${heading}" not found in CHANGELOG.md — add it to the living section before aggregating ${section} fragments`
@@ -125,7 +149,7 @@ export function insertBullets(changelogText, bulletsBySection) {
* Aggregate fragments into CHANGELOG.md. Returns a summary object. When dryRun is
* true nothing is written or deleted.
*/
export function aggregate({ root = ROOT, dryRun = false } = {}) {
export function aggregate({ root = ROOT, dryRun = false, version = readVersion(root) } = {}) {
const collected = collectFragments(root);
if (collected.invalid.length > 0) {
const detail = collected.invalid.map((i) => `${i.file}: ${i.error}`).join("\n");
@@ -134,7 +158,7 @@ export function aggregate({ root = ROOT, dryRun = false } = {}) {
const total = collected.features.length + collected.fixes.length + collected.maintenance.length;
const changelogPath = join(root, "CHANGELOG.md");
const before = readFileSync(changelogPath, "utf8");
const after = total === 0 ? before : insertBullets(before, collected);
const after = total === 0 ? before : insertBullets(before, collected, version);
if (!dryRun && total > 0) {
writeFileSync(changelogPath, after);
for (const section of Object.keys(SECTIONS)) {

View File

@@ -0,0 +1,831 @@
#!/usr/bin/env node
// scripts/release/reconcile-changelog.mjs
//
// Reconcile the living `## [<version>]` CHANGELOG section against the FULL development cycle
// (Phase 0a.10a.3a of /generate-release), so that at the moment a release is cut:
// • every cycle commit is represented by a bullet whose PRIMARY reference is that commit's PR,
// • every bullet carries the merged PR link and `— thanks @author` (Hard Rule #16),
// • fragments are folded in under the RIGHT version section (not the first heading that
// matches — `[Unreleased]` still carries a `### ✨ New Features` heading),
// • fragments that duplicate bullets already shipped in a previous version are dropped,
// • the section opens with "📊 Release by the numbers" + "🏆 Top 25 contributors" (v3.8.50 format).
//
// It never touches bullets that already exist in the section (the changelog-integrity gate
// compares bullet lines against the base), never touches `[Unreleased]`, and never edits any
// other version section. Run `npm run release:contributors -- <version> --inject` afterwards to
// (re)build the `### 🙌 Contributors` table, then `release:sync-changelog-i18n`.
//
// Lessons baked in (v3.8.51 reconciliation, 2026-09-07 — PR #12971):
// • prefix of a fragment filename is NOT a reliable PR number (issue numbers, closed/recreated
// PRs, literal `#PR_NUMBER`); the commit that ADDED the fragment (`git log --diff-filter=A`)
// is the definitive origin — unless that commit is a "carrier" PR that only back-filled
// fragments for other people's PRs (`--carrier N`), then the prefix wins;
// • a commit is covered only when its OWN PR is a primary ref (a `/pull/N` link or the last
// `#N` on the bullet line) — an incidental mention ("Opper #11629 + 1min.ai #11631") must not
// hide a PR's own bullet;
// • gen-contributors only reads lines that start with "- ", so fragment bullets are collapsed
// to a single line (pre-existing section bullets are left verbatim).
//
// Usage:
// node scripts/release/reconcile-changelog.mjs [--version 3.8.51] [--base <ref>] [--head <ref>]
// [--release-branch release/v3.8.51] [--credit 12255=backryun] [--carrier 11938]
// [--drop-fragment changelog.d/fixes/x.md] [--fragment-pr changelog.d/fixes/y.md=11845]
// [--prs <cached gh json>] [--dry-run] [--report <path.json>]
//
// Defaults: version = package.json; release branch = release/v<version>; head = HEAD;
// base = parent of the commit that opened the cycle (first commit carrying the version string,
// see resolveCycleBase in list-uncovered-commits.mjs) — pass `--base origin/release/v<prev>` to
// use the previous release tip explicitly. Exit 0 always (advisory: the captain reviews the diff).
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveCycleBase } from "./list-uncovered-commits.mjs";
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
export const MAINTAINER = "diegosouzapw";
export const SECTION_HEADINGS = Object.freeze({
features: "### ✨ New Features",
fixes: "### 🐛 Bug Fixes",
maintenance: "### 📝 Maintenance",
});
const TYPE_LABEL = {
fix: "🐛 Fixes",
feat: "✨ Features",
docs: "📚 Docs",
chore: "🧹 Chore",
test: "🧪 Tests",
refactor: "♻️ Refactor",
perf: "⚡ Performance",
security: "🔒 Security",
ci: "⚙️ CI",
deps: "📦 Dependencies",
build: "🏗️ Build",
revert: "⏪ Reverts",
other: "🔀 Other",
};
const BOT_RE = /dependabot|\[bot\]|^app\//i;
// ───────────────────────────── pure helpers (unit-tested) ─────────────────────────────
/** `[#N](url)` → `#N`, `[@h](url)` → `@h` so refs/handles can be scanned uniformly. */
export const normalizeLinks = (s) =>
s.replace(/\[#(\d+)\]\([^)]*\)/g, "#$1").replace(/\[@([A-Za-z0-9_-]+)\]\([^)]*\)/g, "@$1");
export const refsIn = (s) => [...s.matchAll(/#(\d+)/g)].map((m) => Number(m[1]));
export const prLink = (repo, n) => `[#${n}](https://github.com/${repo}/pull/${n})`;
/**
* Primary refs of a bullet: every `/pull/N` link, the LAST `#N` on its first line (the
* conventional trailing `(#N)` back-reference) and every explicit `(#N …)` group — a group
* that OPENS with the ref, e.g. `(#11436)` or `(#11436 — thanks @x)`. An incidental mention
* inside prose (`(Opper #11629 + 1min.ai #11631)`) does not count.
*/
export function primaryRefs(bullet) {
const flat = normalizeLinks(bullet.replace(/\n\s+/g, " "));
const out = new Set([...bullet.matchAll(/\/pull\/(\d+)\)/g)].map((m) => Number(m[1])));
const refs = refsIn(flat);
if (refs.length) out.add(refs[refs.length - 1]);
for (const m of flat.matchAll(/\(#(\d+)(?=[\s,)—-])/g)) out.add(Number(m[1]));
return out;
}
/** Commit hashes a bullet documents explicitly as `(direct commit \`<hash>\`)`. */
export const directHashes = (bullet) =>
[...bullet.matchAll(/direct commit `([0-9a-f]{7,40})`/g)].map((m) => m[1]);
/**
* Split a markdown text into bullet blocks per section heading. A block is a `- ` line plus its
* indented continuation lines. `fixedSection` forces every bullet into one section (fragments).
* `collapse` joins continuation lines into the first line (fragments only — never pre-existing
* section bullets, which the changelog-integrity gate compares line by line).
*/
export function parseBlocks(text, { fixedSection = null, collapse = false } = {}) {
const out = { features: [], fixes: [], maintenance: [] };
let cur = fixedSection;
let block = null;
const flush = () => {
if (block && cur) {
out[cur].push(
collapse
? block.map((l, i) => (i ? l.trim() : l.replace(/\s+$/, ""))).join(" ")
: block.join("\n")
);
}
block = null;
};
for (const line of text.split("\n")) {
if (!fixedSection && line.startsWith("### ")) {
flush();
cur = Object.keys(SECTION_HEADINGS).find((k) => SECTION_HEADINGS[k] === line.trim()) || null;
continue;
}
if (line.startsWith("- ")) {
flush();
if (cur) block = [line];
continue;
}
if (block && /^\s+\S/.test(line)) {
block.push(line);
continue;
}
flush();
}
flush();
return out;
}
/** Escape every regex metacharacter (CodeQL js/incomplete-sanitization: never escape just one). */
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const mentions = (b, h) => new RegExp(`@${escapeRegExp(h)}(?![A-Za-z0-9_-])`, "i").test(b);
/** Append `— thanks @a / @b` (or extend an existing trailing thanks group) for handles not yet mentioned. */
export function addCredit(bullet, handles, maintainer = MAINTAINER) {
const hs = [...new Set(handles.filter((h) => h && h !== maintainer && !mentions(bullet, h)))];
if (!hs.length) return bullet;
const lines = bullet.split("\n");
let last = lines[lines.length - 1];
const add = hs.map((h) => `@${h}`).join(" / ");
if (/thanks\s+@[A-Za-z0-9_-]+(\s*\/\s*@[A-Za-z0-9_-]+)*\s*$/.test(last)) {
last = `${last.replace(/\s*$/, "")} / ${add}`;
} else if (/thanks\s+@[A-Za-z0-9_-]+(\s*\/\s*@[A-Za-z0-9_-]+)*\)\s*$/.test(last)) {
last = `${last.replace(/\)\s*$/, "")} / ${add})`;
} else {
last = `${last.replace(/\s*$/, "")} — thanks ${add}`;
}
lines[lines.length - 1] = last;
return lines.join("\n");
}
/** Append a `([#N](…))` link to the last line of a bullet, before any trailing thanks group. */
export function appendLink(bullet, repo, n) {
if (refsIn(normalizeLinks(bullet)).includes(n)) return bullet;
const lines = bullet.split("\n");
let last = lines[lines.length - 1];
const th = last.match(/\s*—\s*thanks\s+@[^\n]*$/);
last = th
? `${last.slice(0, th.index).replace(/\s*$/, "")} (${prLink(repo, n)})${th[0]}`
: `${last.replace(/\s*$/, "")} (${prLink(repo, n)})`;
lines[lines.length - 1] = last;
return lines.join("\n");
}
/** Conventional-commit subject → { type, text } with the type prefix bolded (repo format). */
export function bulletFromSubject(subject, special = {}) {
let s = subject
.replace(/\s*\(#\d+\)\s*$/, "")
.replace(/^\[?URGENT\]?\s*/i, "")
.trim();
if (special[s]) s = special[s];
const m = s.match(/^([a-z]+)(\([^)]*\))?(!)?:\s*(.+)$/i);
if (!m) return { type: "other", text: s };
return { type: m[1].toLowerCase(), text: `**${m[1].toLowerCase()}${m[2] || ""}:** ${m[4]}` };
}
export const sectionForType = (type) =>
type === "feat"
? "features"
: ["fix", "perf", "security", "revert"].includes(type)
? "fixes"
: "maintenance";
/** Type of an existing bullet (`- **fix(x):** …` or `- fix(x): …`), else "other". */
export function typeOfBullet(bullet) {
const m = normalizeLinks(bullet).match(/^- \*{0,2}([a-z]+)(?:\([^)]*\))?!?:\*{0,2}/i);
return m ? m[1].toLowerCase() : "other";
}
/** Text key used to spot twins (two fragments for one PR, a direct-commit twin of a synced PR). */
export const dedupKey = (b) =>
normalizeLinks(b)
.split("\n")[0]
.replace(/\s*\((?:#\d+[^)]*|direct commit[^)]*)\)\s*/g, " ")
.replace(/\s*—\s*thanks.*$/, "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim()
.slice(0, 100);
/**
* Keep one bullet per dedupKey (the best-linked one) and MERGE every `([#N](…))` /
* `(direct commit …)` group the dropped twins carried into the survivor.
*/
export function dedupeBullets(list) {
const best = new Map();
const score = (b) => (b.match(/\/pull\//g) || []).length * 10 + b.length / 1000;
for (const b of list) {
const k = dedupKey(b);
if (!best.has(k) || score(b) > best.get(k).score) best.set(k, { b, score: score(b) });
}
const extras = new Map();
const dropped = [];
for (const b of list) {
const k = dedupKey(b);
if (best.get(k).b === b) continue;
dropped.push(b);
if (!extras.has(k)) extras.set(k, []);
for (const m of b.matchAll(/\(\[#\d+\]\([^)]*\)\)|\(direct commit `[0-9a-f]+`\)/g)) {
extras.get(k).push(m[0]);
}
}
const seen = new Set();
const kept = [];
for (const b of list) {
const k = dedupKey(b);
if (best.get(k).b !== b || seen.has(k)) continue;
seen.add(k);
let out = b;
for (const g of extras.get(k) || []) {
if (out.includes(g)) continue;
const th = out.match(/\s*—\s*thanks[^\n]*$/);
out = th ? `${out.slice(0, th.index)} ${g}${th[0]}` : `${out} ${g}`;
}
kept.push(out);
}
return { kept, dropped };
}
/** Extract `## [version]` … up to the next `## [` (exclusive). */
export function versionSectionRange(changelog, version) {
const esc = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const m = changelog.match(new RegExp(`^## \\[${esc}\\][^\\n]*$`, "m"));
if (!m) return null;
const bodyStart = m.index + m[0].length;
const rest = changelog.slice(bodyStart);
const next = rest.search(/\n## \[/);
return { start: m.index, bodyStart, end: next === -1 ? changelog.length : bodyStart + next };
}
/** Everything under every `## [` heading OTHER than `version` (used to spot already-shipped text). */
export function otherSectionsText(changelog, version) {
const r = versionSectionRange(changelog, version);
if (!r) return changelog;
return changelog.slice(0, r.start) + changelog.slice(r.end);
}
/** Commits covered by the bullets: own PR is a primary ref (or closes a cited issue); no-PR commits by any ref. */
export function computeCoverage(
rows,
bullets,
{ closingPrs = new Set(), originHashes = new Set(), skipHashes = new Set() } = {}
) {
const cited = new Set();
const hashes = [];
for (const b of bullets) {
for (const n of primaryRefs(b)) cited.add(n);
hashes.push(...directHashes(b));
}
const documentedHash = (full) =>
hashes.some((h) => full.startsWith(h) || h.startsWith(full.slice(0, 9)));
const uncovered = rows.filter((r) => {
const h = r.hash.slice(0, 9);
if (skipHashes.has(h) || originHashes.has(h)) return false;
if (r.pr) return !(cited.has(r.pr) || closingPrs.has(r.pr));
return !(documentedHash(r.hash) || r.refs.some((x) => cited.has(x)));
});
return { cited, uncovered };
}
/** Rank authors by commits; key = GitHub login of the merged PR when known, else the mailmap name. */
export function rankAuthors(rows, limit = 25) {
const counts = {};
const names = {};
for (const r of rows) {
if (BOT_RE.test(r.authorName)) continue;
const k = r.prAuthor || r.authorName;
counts[k] = (counts[k] || 0) + 1;
names[k] ??= {};
names[k][r.authorName] = (names[k][r.authorName] || 0) + 1;
}
const display = (k) => Object.entries(names[k]).sort((a, b) => b[1] - a[1])[0][0];
return {
counts,
top: Object.entries(counts)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.slice(0, limit)
.map(([k, c]) => [display(k) === k ? k : `${display(k)} (@${k})`, c]),
};
}
const fmt = (n) => n.toLocaleString("en-US");
const medal = (i) => (i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : String(i + 1));
/** Render the whole `## [version]` section (header → stats → ranking → three sections). */
export function renderSection({
version,
today,
baseTip,
headTip,
rows,
all,
ranking,
prNumbers,
dateSuffix = "TBD",
}) {
const bullets = Object.values(all)
.flat()
.filter((b) => b.startsWith("- "));
const byType = {};
const SEC_DEFAULT = { features: "feat", fixes: "fix", maintenance: "chore" };
for (const k of Object.keys(all)) {
for (const b of all[k].filter((x) => x.startsWith("- "))) {
let t = typeOfBullet(b);
if (!TYPE_LABEL[t] || t === "other") t = SEC_DEFAULT[k];
byType[TYPE_LABEL[t]] = (byType[TYPE_LABEL[t]] || 0) + 1;
}
}
const prRefs = new Set();
const handles = new Set();
for (const b of bullets) {
for (const n of refsIn(normalizeLinks(b))) if (prNumbers.has(n)) prRefs.add(n);
for (const m of normalizeLinks(b).matchAll(/@([A-Za-z0-9_-]+)/g)) handles.add(m[1]);
}
const humans = rows.filter((r) => !BOT_RE.test(r.authorName));
const people = new Set([...Object.keys(ranking.counts), ...handles].map((x) => x.toLowerCase()));
const lines = [
`## [${version}] — ${dateSuffix}`,
``,
`_Living section — reconciled ${today} from all cycle commits (\`${baseTip}\`\`${headTip}\`, ${fmt(rows.length)} non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each \`/generate-release\` phase._`,
``,
`### 📊 Release by the numbers`,
``,
`| | |`,
`| --- | ---: |`,
`| 👥 People who contributed | **${fmt(people.size)}** |`,
`| 📝 Commits in the cycle | **${fmt(rows.length)}** |`,
`| 🔀 Pull requests referenced | **${fmt(prRefs.size)}** |`,
`| 📋 Changelog entries | **${fmt(bullets.length)}** |`,
`| 🙌 Contributors credited in entries | **${fmt(handles.size)}** |`,
`| 🤖 Automated dependency commits | ${rows.length - humans.length} |`,
``,
`**Entries by type**`,
``,
`| Type | Count |`,
`| --- | ---: |`,
...Object.entries(byType)
.sort((a, b) => b[1] - a[1])
.map(([t, c]) => `| ${t} | ${c} |`),
``,
`### 🏆 Top 25 contributors this cycle`,
``,
`_By commits in \`${baseTip}..${headTip}\`, author identities consolidated via \`.mailmap\` and the merged PR's GitHub login. Bots excluded._`,
``,
`| # | Contributor | Commits |`,
`| ---: | --- | ---: |`,
...ranking.top.map(([n, c], i) => `| ${medal(i)} | ${n} | ${c} |`),
``,
];
for (const k of ["features", "fixes", "maintenance"]) {
lines.push(SECTION_HEADINGS[k], "", ...all[k], "");
}
return `${lines.join("\n")}\n---\n`;
}
// ───────────────────────────── data acquisition (git + gh) ─────────────────────────────
const git = (args, cwd = ROOT) =>
execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 }).trim();
const gh = (args) => execFileSync("gh", args, { encoding: "utf8", maxBuffer: 1 << 28 });
export function repoSlug(cwd = ROOT) {
const url = git(["remote", "get-url", "origin"], cwd);
const m = url.match(/github\.com[:/]([^/]+\/[^/.]+)/);
return m ? m[1] : "diegosouzapw/OmniRoute";
}
/** Non-merge commits in `base..head` with mailmap identities, PR number and co-author trailers. */
export function readCommits(base, head, cwd = ROOT) {
const raw = git(
[
"log",
"--no-merges",
"--use-mailmap",
"--date=short",
"--format=%H%x1f%ad%x1f%aN%x1f%aE%x1f%s%x1f%(trailers:key=Co-authored-by,valueonly,separator=%x1e)%x1e%x1e",
`${base}..${head}`,
],
cwd
);
return raw
.split("\x1e\x1e")
.map((s) => s.replace(/^\n/, ""))
.filter((s) => s.trim())
.map((r) => {
const [hash, date, authorName, authorEmail, subject, coa] = r.split("\x1f");
const refs = refsIn(subject);
const prMatch = subject.match(/\(#(\d+)\)\s*$/);
return {
hash,
date,
authorName,
authorEmail,
subject,
refs,
pr: prMatch ? Number(prMatch[1]) : null,
prAuthor: null,
coauthors: (coa || "")
.split("\x1e")
.map((s) => s.trim())
.filter(Boolean),
};
});
}
export function fetchMergedPrs(repo, releaseBranch) {
return JSON.parse(
gh([
"pr",
"list",
"--repo",
repo,
"--state",
"merged",
"--base",
releaseBranch,
"--limit",
"1000",
"--json",
"number,title,author,body,closingIssuesReferences,mergedAt",
])
);
}
export function fetchPr(repo, n) {
try {
return JSON.parse(
execFileSync(
"gh",
[
"pr",
"view",
String(n),
"--repo",
repo,
"--json",
"number,title,author,body,closingIssuesReferences",
],
{
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}
)
);
} catch {
return null;
}
}
/** Fragment files at `ref` + the commit that ADDED each one (definitive origin of the credit). */
export function readFragments(ref = "HEAD", cwd = ROOT) {
const out = [];
for (const dir of Object.keys(SECTION_HEADINGS)) {
let files = "";
try {
files = git(["ls-tree", "--name-only", ref, `changelog.d/${dir}/`], cwd);
} catch {
continue;
}
for (const f of files
.split("\n")
.filter((x) => x && !/README\.md$|\.gitkeep$/.test(x))
.sort()) {
const text = git(["show", `${ref}:${f}`], cwd);
const origin =
git(["log", "--diff-filter=A", "--format=%h%x09%s", "--", f], cwd)
.split("\n")
.filter(Boolean)
.pop() || "";
const [ohash, osubject = ""] = origin.split("\t");
const om = osubject.match(/\(#(\d+)\)\s*$/);
const pm = f.match(/\/(\d{4,6})-/);
out.push({
path: f,
section: dir,
text,
originHash: ohash ? ohash.slice(0, 9) : null,
originPr: om ? Number(om[1]) : null,
prefixPr: pm ? Number(pm[1]) : null,
});
}
}
return out;
}
// ───────────────────────────── the reconciliation itself ─────────────────────────────
export function reconcile({
changelog,
version,
repo,
rows,
prs,
fragments,
extraPrs = new Map(),
credits = {},
carriers = new Set(),
dropFragments = new Set(),
fragmentPr = {},
fragmentCredit = {},
today,
baseTip,
headTip,
skipHashes = new Set(),
special = {},
}) {
const prBy = new Map(prs.map((p) => [p.number, p]));
for (const [n, p] of extraPrs) if (!prBy.has(n)) prBy.set(n, p);
const prAuthor = (n) => prBy.get(n)?.author?.login || null;
for (const r of rows) r.prAuthor = r.pr ? prAuthor(r.pr) : null;
const issueToPrs = new Map();
for (const p of prs)
for (const ci of p.closingIssuesReferences || []) {
if (!issueToPrs.has(ci.number)) issueToPrs.set(ci.number, []);
issueToPrs.get(ci.number).push(p.number);
}
const range = versionSectionRange(changelog, version);
if (!range) throw new Error(`CHANGELOG.md has no "## [${version}]" section`);
const existing = parseBlocks(changelog.slice(range.bodyStart, range.end));
const shippedElsewhere = normalizeLinks(otherSectionsText(changelog, version));
const blocks = {
features: [...existing.features],
fixes: [...existing.fixes],
maintenance: [...existing.maintenance],
};
const srcOf = new Map();
for (const f of fragments) {
const parsed = parseBlocks(f.text, { fixedSection: f.section, collapse: true });
for (const b of parsed[f.section]) {
blocks[f.section].push(b);
srcOf.set(b, f);
}
}
const dropped = [];
const patched = [];
const mismatches = [];
const originHashes = new Set();
for (const k of Object.keys(blocks)) {
const out = [];
for (let b of blocks[k]) {
const frag = srcOf.get(b);
const first = b.split("\n")[0].trim();
if (frag && dropFragments.has(frag.path)) {
dropped.push({ why: "dropped by --drop-fragment", first, src: frag.path });
continue;
}
const probe = normalizeLinks(first)
.replace(/\s*\(#\d+.*$/, "")
.slice(0, 90);
if (frag && probe.length > 40 && shippedElsewhere.includes(probe)) {
dropped.push({
why: "text already shipped in another version section",
first,
src: frag.path,
});
continue;
}
if (frag) {
const refs0 = refsIn(normalizeLinks(b));
const override = fragmentPr[frag.path];
const oPr = frag.originPr;
const fPr = frag.prefixPr;
let defPrs = override
? [override]
: oPr && prBy.has(oPr) && !carriers.has(oPr)
? [oPr]
: fPr && prBy.has(fPr)
? [fPr]
: oPr && prBy.has(oPr)
? [oPr]
: [];
if (!defPrs.length)
for (const n of refs0) for (const pr of issueToPrs.get(n) || []) defPrs.push(pr);
defPrs = [...new Set(defPrs)];
if (oPr && fPr && prBy.has(fPr) && oPr !== fPr) {
mismatches.push(
`${frag.path}: prefix #${fPr} (${prAuthor(fPr)}) vs origin #${oPr} (${prAuthor(oPr)}) → used #${defPrs.join("/")}`
);
}
if (b.includes("#PR_NUMBER") && defPrs.length) {
b = b
.replace(/\[#PR_NUMBER\]\([^)]*\)/g, prLink(repo, defPrs[0]))
.replace(/#PR_NUMBER/g, `#${defPrs[0]}`);
}
for (const h of fragmentCredit[frag.path] || []) b = addCredit(b, [h]);
for (const n of defPrs) {
const before = b;
b = appendLink(b, repo, n);
b = addCredit(b, [prAuthor(n), ...(credits[n] || [])]);
if (b !== before) patched.push(`${frag.path} → #${n} @${prAuthor(n)}`);
}
if (!override && !(oPr && carriers.has(oPr)) && frag.originHash)
originHashes.add(frag.originHash);
}
out.push(b);
}
blocks[k] = out;
}
const allExisting = Object.values(blocks)
.flat()
.filter((b) => b.startsWith("- "));
const closingPrs = new Set();
const citedIssues = new Set(allExisting.flatMap((b) => refsIn(normalizeLinks(b))));
for (const p of prs)
for (const ci of p.closingIssuesReferences || [])
if (citedIssues.has(ci.number)) closingPrs.add(p.number);
const { uncovered } = computeCoverage(rows, allExisting, {
closingPrs,
originHashes,
skipHashes,
});
const gen = { features: [], fixes: [], maintenance: [] };
const deps = [];
for (const r of [...uncovered].sort(
(a, b) => (a.pr || 0) - (b.pr || 0) || a.date.localeCompare(b.date)
)) {
if (BOT_RE.test(r.authorName)) {
deps.push(r);
continue;
}
const c = bulletFromSubject(r.subject, special);
let bullet = `- ${c.text}`;
bullet += r.pr ? ` (${prLink(repo, r.pr)})` : ` (direct commit \`${r.hash.slice(0, 10)}\`)`;
bullet = addCredit(bullet, [
r.pr ? prAuthor(r.pr) : null,
...(r.pr ? credits[r.pr] || [] : []),
]);
gen[sectionForType(c.type)].push(bullet);
}
if (deps.length) {
gen.maintenance.push(
`- **deps:** ${deps.length} Dependabot bumps — ${deps
.map(
(r) =>
`${r.subject.replace(/^deps(\([^)]*\))?:\s*/, "").replace(/\s*\(#\d+\)\s*$/, "")} (${prLink(repo, r.pr)})`
)
.join("; ")}`
);
}
const all = {};
const dedupDropped = [];
for (const k of Object.keys(blocks)) {
const existingSet = new Set(existing[k]);
const merged = [...blocks[k], ...gen[k]];
const kept = merged.filter((b) => existingSet.has(b));
const { kept: rest, dropped: dd } = dedupeBullets(merged.filter((b) => !existingSet.has(b)));
dedupDropped.push(...dd);
all[k] = [...kept, ...rest];
}
const ranking = rankAuthors(rows);
const section = renderSection({
version,
today,
baseTip,
headTip,
rows,
all,
ranking,
prNumbers: new Set(prBy.keys()),
});
const next = `${changelog.slice(0, range.start)}${section}${changelog.slice(range.end + 1)}`;
const report = {
version,
baseTip,
headTip,
commits: rows.length,
existing: Object.fromEntries(Object.entries(existing).map(([k, v]) => [k, v.length])),
fragments: fragments.length,
generated: Object.fromEntries(Object.entries(gen).map(([k, v]) => [k, v.length])),
bullets: Object.values(all)
.flat()
.filter((b) => b.startsWith("- ")).length,
dropped,
patched: patched.length,
mismatches,
dedupDropped,
uncovered: uncovered.length,
depsRolled: deps.length,
ranking: ranking.top,
};
return { changelog: next, report };
}
// ───────────────────────────── CLI ─────────────────────────────
function parseArgs(argv) {
const o = {
credit: {},
carriers: new Set(),
dropFragments: new Set(),
fragmentPr: {},
dryRun: false,
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const v = () => argv[++i];
if (a === "--version") o.version = v();
else if (a === "--base") o.base = v();
else if (a === "--head") o.head = v();
else if (a === "--release-branch") o.releaseBranch = v();
else if (a === "--prs") o.prs = v();
else if (a === "--report") o.report = v();
else if (a === "--dry-run") o.dryRun = true;
else if (a === "--carrier") o.carriers.add(Number(v()));
else if (a === "--drop-fragment") o.dropFragments.add(v());
else if (a === "--credit") {
const [n, hs] = v().split("=");
o.credit[Number(n)] = hs.split(",").map((h) => h.replace(/^@/, ""));
} else if (a === "--fragment-pr") {
const [p, n] = v().split("=");
o.fragmentPr[p] = Number(n);
}
}
return o;
}
export function main(argv = process.argv.slice(2)) {
const o = parseArgs(argv);
const version =
o.version || JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
const releaseBranch = o.releaseBranch || `release/v${version}`;
const head = o.head || "HEAD";
let base = o.base;
if (!base) {
const { base: open, source } = resolveCycleBase(version);
base = `${open}^`;
console.log(`[reconcile-changelog] base = parent of the ${source} commit ${open.slice(0, 10)}`);
}
const repo = repoSlug();
const rows = readCommits(base, head);
const prs = o.prs
? JSON.parse(fs.readFileSync(o.prs, "utf8"))
: fetchMergedPrs(repo, releaseBranch);
const known = new Set(prs.map((p) => p.number));
const extraPrs = new Map();
for (const n of new Set(rows.filter((r) => r.pr && !known.has(r.pr)).map((r) => r.pr))) {
const p = fetchPr(repo, n);
if (p) extraPrs.set(n, p);
}
const fragments = readFragments(head);
const changelog = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8");
const baseTip = git(["rev-parse", "--short=10", base]);
const headTip = git(["rev-parse", "--short=10", head]);
// the cycle-open bump and the living-section restore are the only legitimate non-bullet commits
const skipHashes = new Set(
rows
.filter(
(r) =>
/^chore\(release\): (open v[\d.]+ development cycle|restore the living)/.test(
r.subject
) || /^Release v[\d.]+$/.test(r.subject)
)
.map((r) => r.hash.slice(0, 9))
);
const { changelog: next, report } = reconcile({
changelog,
version,
repo,
rows,
prs,
fragments,
extraPrs,
credits: o.credit,
carriers: o.carriers,
dropFragments: o.dropFragments,
fragmentPr: o.fragmentPr,
today: new Date().toISOString().slice(0, 10),
baseTip,
headTip,
skipHashes,
});
if (!o.dryRun) {
fs.writeFileSync(path.join(ROOT, "CHANGELOG.md"), next);
for (const f of fragments) {
try {
fs.unlinkSync(path.join(ROOT, f.path));
} catch {
/* already gone */
}
}
}
if (o.report) fs.writeFileSync(o.report, JSON.stringify(report, null, 1));
const { dropped, mismatches, dedupDropped, ranking, ...summary } = report;
console.log(`[reconcile-changelog] ${o.dryRun ? "(dry-run) " : ""}${JSON.stringify(summary)}`);
for (const d of dropped) console.log(` dropped: ${d.why}${d.first.slice(0, 90)}`);
for (const m of mismatches) console.log(` review: ${m}`);
for (const d of dedupDropped) console.log(` deduped: ${d.slice(0, 90)}`);
console.log(
`[reconcile-changelog] next: npm run release:contributors -- ${version} --inject && npx prettier --write CHANGELOG.md && npm run release:sync-changelog-i18n -- ${version} <prev> && npm run check:changelog-integrity`
);
return 0;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
process.exit(main());
}

View File

@@ -37,12 +37,12 @@ omniroute tunnel
omniroute tunnel list
```
### `tunnel create [type]`
### `tunnel create`
**Example:**
```bash
omniroute tunnel create [type]
omniroute tunnel create
```
### `tunnel stop <type>`

View File

@@ -526,7 +526,7 @@ function ComboHealthCard({
<MetricBlock
icon="battery_status_good"
label={t("comboHealthWorstQuotaLeft")}
value={formatPercent(combo.quotaHealth.worstRemainingPct)}
value={formatPercentOrDash(combo.quotaHealth.worstRemainingPct)}
/>
<MetricBlock
icon="balance"
@@ -560,7 +560,8 @@ function ComboHealthCard({
<div className="flex flex-col gap-3">
{combo.quotaHealth.providers.map((provider) => {
const trendMeta = getTrendMeta(provider.trend);
const width = `${Math.max(provider.remainingPct, provider.remainingPct > 0 ? 6 : 0)}%`;
const pct = provider.remainingPct;
const width = `${pct === null ? 0 : Math.max(pct, pct > 0 ? 6 : 0)}%`;
return (
<div
@@ -574,7 +575,7 @@ function ComboHealthCard({
</div>
<div className="mt-1 text-xs text-text-muted">
{t("comboHealthRemainingQuota", {
value: formatPercent(provider.remainingPct, 1),
value: formatPercentOrDash(provider.remainingPct, 1),
})}
</div>
</div>

View File

@@ -14,6 +14,7 @@ import { NextResponse } from "next/server";
* public on an exposed instance, so version, uptime and memory stay behind the authenticated
* `/api/monitoring/health`. For a probe that also confirms the database answers, use
* `/api/health/ping`.
* Readiness (DB-backed) lives at /api/health/ping (pingDb, 503 when down); this route stays liveness-only so a slow DB never restarts the container.
*/
export const dynamic = "force-dynamic";

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { getProviderConnections } from "@/lib/db/providers";
import { getCachedSettings } from "@/lib/db/readCache";
import { getWalMaintenanceState } from "@/lib/db/walMaintenance";
import { buildHealthPayload } from "@/lib/monitoring/observability";
import { readRunningBuildSha } from "@/lib/monitoring/buildSha";
import { APP_CONFIG } from "@/shared/constants/config";
@@ -241,6 +242,10 @@ async function rebuildHealthPayload(): Promise<unknown> {
null
)
: null;
// #12853: WAL maintenance state (ticks/busy streak + totals) next to the
// admission gates. getWalMaintenanceState never throws and never touches
// the DB — a monitoring read stays cheap. Additive key, nothing moves.
const walMaintenance = readHealthValue("wal maintenance", () => getWalMaintenanceState(), null);
const payload = buildHealthPayload({
appVersion: APP_CONFIG.version,
@@ -266,6 +271,7 @@ async function rebuildHealthPayload(): Promise<unknown> {
credentialHealth,
adaptiveAdmission,
chatAdmission,
walMaintenance,
});
if (generation === healthPayloadCacheGeneration) {

View File

@@ -4,7 +4,7 @@ import pino from "pino";
import { buildErrorBody } from "@omniroute/open-sse/utils/error.ts";
import { getProviderMetrics } from "@/lib/db/callLogStats";
import { toNumber } from "@/shared/utils/numeric";
import { toNumber, toNumberOrNull } from "@/shared/utils/numeric";
const logger = pino({ name: "provider-metrics-api" });
@@ -22,7 +22,7 @@ export async function GET() {
totalRequests: number;
totalSuccesses: number;
successRate: number;
avgLatencyMs: number;
avgLatencyMs: number | null;
lastRequestAt: string | null;
lastErrorAt: string | null;
lastStatus: number | null;
@@ -41,7 +41,7 @@ export async function GET() {
: "unknown";
const totalRequests = toNumber(row.totalRequests);
const totalSuccesses = toNumber(row.totalSuccesses);
const avgLatencyMs = toNumber(row.avgLatencyMs);
const avgLatencyMs = toNumberOrNull(row.avgLatencyMs);
const lastRequestAt = typeof row.lastRequestAt === "string" ? row.lastRequestAt : null;
const lastErrorAt = typeof row.lastErrorAt === "string" ? row.lastErrorAt : null;
const lastStatus = row.lastStatus == null ? null : toNumber(row.lastStatus);
@@ -66,8 +66,7 @@ export async function GET() {
// Only flag as errorProvider if the provider's MOST RECENT request was itself
// a failure. A provider with a historical lastErrorAt but a recent success
// (lastStatus 2xx/3xx) must not be shown as currently errored (#3619).
const isCurrentlyInError =
lastStatus !== null && (lastStatus < 200 || lastStatus >= 400);
const isCurrentlyInError = lastStatus !== null && (lastStatus < 200 || lastStatus >= 400);
const errorTs = isCurrentlyInError && lastErrorAt ? Date.parse(lastErrorAt) : 0;
if (Number.isFinite(errorTs) && errorTs > errorProviderTs) {
errorProvider = provider;

View File

@@ -16,7 +16,7 @@ export async function GET(request: Request) {
const providers: Record<
string,
{ requests: number; avg_latency_ms: number; total_cost: number }
{ requests: number; avg_latency_ms: number | null; total_cost: number }
> = {};
for (const row of providerStats) {
const costPerQuery = SEARCH_PROVIDERS[row.provider]?.costPerQuery || 0;

View File

@@ -25,6 +25,7 @@ import {
} from "@/lib/system/versionCheck";
import { resolveGlobalOmniroutePath } from "@/lib/system/globalPackagePath";
import { restartRunningServer } from "@/lib/system/processManagerRestart";
import { APP_CONFIG } from "@/shared/constants/appConfig";
// #5542 — On Windows npm is `npm.cmd`; Node ≥24 refuses to execFile a `.cmd` without
// a shell (nodejs/node#52554 → "spawn npm ENOENT"). buildNpmExecOptions enables the
// shell on win32 only; SERVICE_VERSION_PATTERN keeps the shell-joined version safe.
@@ -35,11 +36,7 @@ const execFileAsync = promisify(execFile);
export const dynamic = "force-dynamic";
function getCurrentVersion(): string {
try {
return require("../../../../../package.json").version as string;
} catch {
return "unknown";
}
return APP_CONFIG.version;
}
/**

View File

@@ -31,10 +31,10 @@ export interface ComboControlCenterHealth {
totalRequests?: number;
};
quotaHealth?: {
worstRemainingPct?: number;
worstRemainingPct?: number | null;
providers?: Array<{
provider: string;
remainingPct: number;
remainingPct: number | null;
isExhausted: boolean;
trend: "improving" | "stable" | "declining";
}>;

View File

@@ -18,7 +18,7 @@ export interface ProviderMetricRow {
provider: string;
totalRequests: number;
totalSuccesses: number;
avgLatencyMs: number;
avgLatencyMs: number | null;
lastRequestAt: string | null;
lastErrorAt: string | null;
lastStatus: number | null;
@@ -37,7 +37,7 @@ export interface ProviderUsageRow {
export interface SearchProviderStatRow {
provider: string;
requests: number;
avg_latency_ms: number;
avg_latency_ms: number | null;
}
export interface SearchRecentRow {
@@ -126,10 +126,9 @@ export function getProviderMetrics(): ProviderMetricRow[] {
*
* Deliberately NOT `getProviderMetrics()` with a `since` parameter: that query
* carries two correlated subqueries (`lastStatus`, `lastErrorStatus`) which a
* ranking never displays, and they dominate its cost — `call_logs` is indexed
* on `timestamp` alone, so each correlated pass rescans the whole window per
* provider. Here a single bounded `GROUP BY` uses `idx_cl_timestamp` and stops
* there. The rules are shared with its neighbour, not the query: same success
* ranking never displays, and they dominate its cost. Here a single bounded
* `GROUP BY` leans on `idx_cl_timestamp` plus `idx_cl_provider_timestamp` /
* `idx_cl_request_provider` (migration 174) and stops there. The rules are shared with its neighbour, not the query: same success
* definition, same `#10714` guard against providers whose connections are gone.
*/
export function getProviderUsageSince(since: string): ProviderUsageRow[] {
@@ -259,17 +258,17 @@ export function getFallbackStats(
.prepare(
`
SELECT
SUM(CASE WHEN (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END) as total,
SUM(CASE WHEN requested_model IS NOT NULL AND requested_model != '' AND (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END) as with_requested,
SUM(CASE
COALESCE(SUM(CASE WHEN (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END), 0) as total,
COALESCE(SUM(CASE WHEN requested_model IS NOT NULL AND requested_model != '' AND (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END), 0) as with_requested,
COALESCE(SUM(CASE
WHEN (combo_name IS NULL OR combo_name = '')
AND requested_model IS NOT NULL
AND requested_model != ''
AND model IS NOT NULL
AND model != ''
THEN 1 ELSE 0 END
) as fallback_eligible,
SUM(CASE
), 0) as fallback_eligible,
COALESCE(SUM(CASE
WHEN (combo_name IS NULL OR combo_name = '')
AND requested_model IS NOT NULL
AND requested_model != ''
@@ -277,7 +276,7 @@ export function getFallbackStats(
AND model != ''
AND LOWER(CASE WHEN instr(requested_model, '/') > 0 THEN substr(requested_model, instr(requested_model, '/') + 1) ELSE requested_model END) != LOWER(model)
THEN 1 ELSE 0 END
) as fallbacks
), 0) as fallbacks
FROM call_logs
${whereClause}
`
@@ -289,8 +288,9 @@ export function getFallbackStats(
/**
* Failure-family breakdown over `call_logs` for the usage analytics endpoint.
* Failures are rows with status >= 400 or a non-empty error summary; successes
* are excluded in SQL. Pre-migration rows and failures the classifier does not
* recognize (null family) land in the explicit `unclassified` bucket.
* are excluded in SQL. Rows predating migration 158 (`error_type` NULL,
* `timestamp` before 2026-08-20) land in `pre_migration`; other NULL families
* land in `unclassified`.
*
* @param whereClause - SQL WHERE clause (may be empty string) using the same
* named params as the usage_history queries.
@@ -305,7 +305,9 @@ export function getErrorTypeBreakdown(
.prepare(
`
SELECT
COALESCE(error_type, 'unclassified') AS errorType,
-- '2026-08-20' = commit 4c15c05f9 that added error_type (migration 158).
-- Lower bound, not exact: late upgraders have post-cutoff rows with NULL values.
CASE WHEN error_type IS NULL AND timestamp < '2026-08-20' THEN 'pre_migration' WHEN error_type IS NULL THEN 'unclassified' ELSE error_type END AS errorType,
COUNT(*) AS count
FROM call_logs
${whereClause} ${whereClause ? "AND" : "WHERE"} (status >= 400 OR error_summary IS NOT NULL)

View File

@@ -41,6 +41,14 @@ import { rowToCamel } from "./caseMapping";
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
import { parseModelAccessMode } from "./apiKeys/modelAccessMode";
import { getExistingDbInstance as getDb, setDbInstance as setDb } from "./singleton";
import type { WalCheckpointMode } from "./walMaintenance";
import {
startWalMaintenance,
stopWalMaintenance,
runCheckpointNow,
getWalMaintenanceState,
logCheckpointOutcome,
} from "./walMaintenance";
// Re-exported so existing call sites that pull these helpers off the core module keep working.
export { toSnakeCase, toCamelCase, objToSnake, rowToCamel, cleanNulls } from "./caseMapping";
import {
@@ -55,7 +63,6 @@ import {
type SqliteDatabase = SqliteAdapter;
type JsonRecord = Record<string, unknown>;
type CheckpointMode = "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE";
type DatabaseOptimizationSettings = DatabaseSettings["optimization"];
type PreservedTableSnapshot = {
table: string;
@@ -400,6 +407,8 @@ const SCHEMA_SQL = `
);
CREATE INDEX IF NOT EXISTS idx_cl_timestamp ON call_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_cl_status ON call_logs(status);
CREATE INDEX IF NOT EXISTS idx_cl_provider_timestamp ON call_logs(provider, timestamp);
CREATE INDEX IF NOT EXISTS idx_cl_request_provider ON call_logs(request_type, provider);
CREATE TABLE IF NOT EXISTS proxy_logs (
id TEXT PRIMARY KEY,
@@ -529,12 +538,6 @@ declare global {
var __omnirouteDbOomFailureCount: number | undefined;
}
function checkpointDb(db: SqliteDatabase, mode: CheckpointMode = "TRUNCATE"): boolean {
if (isCloud || isBuildPhase || !SQLITE_FILE) return false;
db.pragma(`wal_checkpoint(${mode})`);
return true;
}
function summarizePreservedTables(tables: PreservedTableSnapshot[]): string {
if (tables.length === 0) return "none";
return tables.map((table) => `${table.table}(${table.rowCount})`).join(", ");
@@ -962,50 +965,9 @@ function startDbHealthCheckScheduler(db: SqliteDatabase) {
dbHealthCheckTimer.unref?.();
}
let walTruncateTimer: NodeJS.Timeout | null = null;
function getWalTruncateIntervalMs(): number {
const rawValue = process.env.OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS;
if (typeof rawValue === "string" && rawValue.trim().length > 0) {
const parsed = Number(rawValue);
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed;
}
}
return 6 * 60 * 60 * 1000;
}
function clearWalTruncateScheduler() {
if (walTruncateTimer) {
clearInterval(walTruncateTimer);
walTruncateTimer = null;
}
}
// Auto-checkpoint moves WAL pages back into the main DB file but never shrinks the WAL
// file itself; only wal_checkpoint(TRUNCATE) does, and a long-running server never closes its DB.
function startWalTruncateScheduler(db: SqliteDatabase) {
clearWalTruncateScheduler();
if (isCloud || isBuildPhase || isAutomatedTestProcess()) return;
const intervalMs = getWalTruncateIntervalMs();
if (intervalMs <= 0) return;
walTruncateTimer = setInterval(() => {
try {
if (!db.open) return;
// TRUNCATE waits for readers; under concurrent write load it can no-op without
// shrinking the file. That is expected — it retries on the next tick.
if (checkpointDb(db, "TRUNCATE")) {
console.log("[DB] Periodic SQLite WAL checkpoint completed (TRUNCATE).");
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.warn("[DB] Periodic WAL truncate failed:", message);
}
}, intervalMs);
walTruncateTimer.unref?.();
}
// The scheduler lives in ./walMaintenance (periodic TRUNCATE + busy warn + PASSIVE retry).
export function runManagedDbHealthCheck(options?: { autoRepair?: boolean }) {
const db = getDbInstance();
@@ -1390,7 +1352,7 @@ export function getDbInstance(): SqliteDatabase {
}
startDbHealthCheckScheduler(db);
startWalTruncateScheduler(db);
startWalMaintenance(db, SQLITE_FILE);
// Log the resolved absolute DATA_DIR + SQLITE_FILE once at init so a
// multi-replica / Docker volume-topology mismatch (each replica opening a
// different on-disk DB → "phantom"/missing combos & connections) is
@@ -1416,9 +1378,10 @@ export function pingDb(): boolean {
}
}
export function closeDbInstance(options?: { checkpointMode?: CheckpointMode | null }): boolean {
export function closeDbInstance(options?: { checkpointMode?: WalCheckpointMode | null }): boolean {
clearDbHealthCheckScheduler();
clearWalTruncateScheduler();
const streakBefore = getWalMaintenanceState().busyStreak;
stopWalMaintenance();
const db = getDb();
if (!db) return false;
@@ -1427,9 +1390,12 @@ export function closeDbInstance(options?: { checkpointMode?: CheckpointMode | nu
try {
if (checkpointMode) {
try {
if (checkpointDb(db, checkpointMode)) {
console.log(`[DB] SQLite WAL checkpoint completed (${checkpointMode}).`);
}
const outcome = runCheckpointNow(db, checkpointMode, {
sqliteFile: SQLITE_FILE,
isCloud,
isBuildPhase,
});
logCheckpointOutcome(outcome, checkpointMode, streakBefore);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.warn(`[DB] WAL checkpoint failed during close (${checkpointMode}):`, message);

View File

@@ -0,0 +1,6 @@
-- GROUP BY provider support. (provider,timestamp) backs the bare
-- GROUP BY provider in getProviderMetrics; (request_type,provider)
-- backs WHERE request_type='search' GROUP BY provider. Non-covering for the
-- real queries (duration/status outside the index) by design — no third index.
CREATE INDEX IF NOT EXISTS idx_cl_provider_timestamp ON call_logs(provider, timestamp);
CREATE INDEX IF NOT EXISTS idx_cl_request_provider ON call_logs(request_type, provider);

View File

@@ -39,6 +39,7 @@ import { pickCodexConnectionForUser } from "@/lib/oauth/utils/codexConnectionSel
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
import { reconcileCodexUsageHistory } from "./providers/usageIdentityReconciliation";
import { isRuntimeRetiredProviderId } from "@/shared/constants/providerRetirement";
import { applyCodexChildCooldownClearOnUpdate } from "./providers/codexAccountState";
/**
* normalizeProviderSpecificData + the Codex fingerprint-seed invariant: Codex
@@ -951,11 +952,14 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
...data,
updatedAt: new Date().toISOString(),
};
merged.providerSpecificData = normalizeConnectionProviderSpecificData(
toStringOrNull(merged.provider),
merged.providerSpecificData,
merged,
existingCamel.providerSpecificData
merged.providerSpecificData = applyCodexChildCooldownClearOnUpdate(
data,
normalizeConnectionProviderSpecificData(
toStringOrNull(merged.provider),
merged.providerSpecificData,
merged,
existingCamel.providerSpecificData
)
);
// Mirror the sanitization the create path applies — keep the returned
// object in lockstep with what we persist.
@@ -1024,65 +1028,13 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
export {
updateCodexScopedQuotaState,
updateCodexScopeCooldown,
applyCodexChildCooldownClearOnUpdate,
stripCodexChildCooldownFields,
stripCodexChildCooldownsFromConnection,
hasCodexScopeCooldown,
liftCodexScopeCooldownOnHeadroom,
} from "./providers/codexAccountState";
/**
* Atomic conditional clear of recoverable error state on a connection row.
*
* Returns true when the row was cleared, false when a concurrent writer
* (markAccountUnavailable, connectionRecovery tick, test, etc.) changed the
* row between the caller's snapshot read and this UPDATE — in which case the
* clear is skipped to preserve the freshest error state. Closes the TOCTOU
* window in the quota-recovery path.
*
* CAS token = (test_status, last_error_at, rate_limited_until).
* markAccountUnavailable always bumps last_error_at on every cooldown/error
* write, so an unchanged last_error_at reliably indicates no concurrent write.
*/
export async function clearConnectionErrorIfUnchanged(
id: string,
expected: {
testStatus: string | null | undefined;
lastErrorAt: string | null | undefined;
rateLimitedUntil: string | null | undefined;
}
): Promise<boolean> {
const db = getDbInstance() as unknown as DbLike;
const result = db
.prepare(
`
UPDATE provider_connections SET
test_status = 'active',
last_error = NULL,
last_error_at = NULL,
last_error_type = NULL,
last_error_source = NULL,
error_code = NULL,
rate_limited_until = NULL,
backoff_level = 0,
updated_at = ?
WHERE id = ?
AND IFNULL(test_status, '') = ?
AND IFNULL(last_error_at, '') = ?
AND IFNULL(rate_limited_until, '') = ?
`
)
.run(
new Date().toISOString(),
id,
expected.testStatus ?? "",
expected.lastErrorAt ?? "",
expected.rateLimitedUntil ?? ""
);
const applied = (result.changes ?? 0) > 0;
if (applied) {
backupDbFile("pre-write");
invalidateDbCache("connections");
bumpProxyConfigGeneration();
}
return applied;
}
/**
* Lightweight stat bump — updates lastUsedAt and consecutiveUseCount without
* SELECT, re-encrypt, cache invalidation, or file backup.
@@ -1183,4 +1135,5 @@ export {
formatResetCountdown,
isConnectionRateLimited,
getRateLimitedConnections,
clearConnectionErrorIfUnchanged,
} from "./providers/rateLimit";

View File

@@ -18,10 +18,105 @@ interface DbLike {
type CodexScopedQuotaPatch = {
quotaState?: JsonRecord;
exhaustedWindow?: "5h" | "7d" | null;
rateLimitedUntil?: string;
rateLimitedUntil?: string | null;
rateLimitSource?: "fallback" | "quota_reset";
};
const CODEX_CHILD_COOLDOWN_KEYS = [
"codexScopeRateLimitedUntil",
"codexScopeRateLimitSource",
] as const;
function omitEmptyRecord(record: JsonRecord): JsonRecord | undefined {
return Object.keys(record).length > 0 ? record : undefined;
}
/** Drop nested Codex child cooldowns; keep quota snapshots and unrelated keys. */
export function stripCodexChildCooldownFields(psd: JsonRecord): JsonRecord {
if (!connectionHasCodexChildCooldown(psd)) return psd;
const next = { ...psd };
for (const key of CODEX_CHILD_COOLDOWN_KEYS) delete next[key];
return next;
}
/** PUT/CAS payload that clears the parent column must also drop nested maps. */
export function applyCodexChildCooldownClearOnUpdate<T extends JsonRecord | undefined>(
data: JsonRecord,
psd: T
): T {
if (psd == null) return psd;
if (!Object.hasOwn(data, "rateLimitedUntil")) return psd;
if (data.rateLimitedUntil != null && data.rateLimitedUntil !== "") return psd;
return stripCodexChildCooldownFields(psd) as T;
}
function connectionHasCodexChildCooldown(psd: JsonRecord): boolean {
return "codexScopeRateLimitedUntil" in psd || "codexScopeRateLimitSource" in psd;
}
/**
* Persist a full-parent cooldown lift into the nested Codex child maps.
* When `alsoClearTopLevel` is set, the parent `rate_limited_until` column is
* nulled in the same transaction so a crash between the two writes cannot
* leave a nested child map behind a cleared parent column.
*/
export function stripCodexChildCooldownsFromConnection(
id: string,
options?: { alsoClearTopLevel?: boolean }
): void {
if (typeof id !== "string" || id.length === 0) return;
const db = getDbInstance() as unknown as DbLike;
const alsoClearTopLevel = options?.alsoClearTopLevel === true;
const candidate = db
.prepare("SELECT provider FROM provider_connections WHERE id = ?")
.get(id);
const isCodex = toRecord(candidate).provider === "codex";
if (!alsoClearTopLevel && !isCodex) return;
backupDbFile("pre-write");
const wrote = db.transaction(() => {
const existing = db
.prepare(
"SELECT provider, provider_specific_data FROM provider_connections WHERE id = ?"
)
.get(id);
if (!existing) return false;
const existingRecord = toRecord(rowToCamel(existing));
const providerSpecificData = toRecord(existingRecord.providerSpecificData);
const stripNested =
existingRecord.provider === "codex" &&
connectionHasCodexChildCooldown(providerSpecificData);
if (!alsoClearTopLevel && !stripNested) return false;
const now = new Date().toISOString();
if (alsoClearTopLevel && stripNested) {
db.prepare(
`UPDATE provider_connections
SET rate_limited_until = NULL,
provider_specific_data = ?,
updated_at = ?
WHERE id = ?`
).run(JSON.stringify(stripCodexChildCooldownFields(providerSpecificData)), now, id);
return true;
}
if (alsoClearTopLevel) {
db.prepare(
`UPDATE provider_connections
SET rate_limited_until = NULL, updated_at = ?
WHERE id = ?`
).run(now, id);
return true;
}
db.prepare(
`UPDATE provider_connections
SET provider_specific_data = ?, updated_at = ?
WHERE id = ?`
).run(JSON.stringify(stripCodexChildCooldownFields(providerSpecificData)), now, id);
return true;
})();
if (wrote) invalidateDbCache("connections");
}
/**
* Atomically merge one virtual Codex child's quota evidence into its persisted parent.
* The transaction reads the latest row so sibling child state cannot be lost.
@@ -70,28 +165,35 @@ export async function updateCodexScopedQuotaState(
}
}
if (patch.rateLimitedUntil) {
const scopeCooldowns = toRecord(providerSpecificData.codexScopeRateLimitedUntil);
const sourceByScope = toRecord(providerSpecificData.codexScopeRateLimitSource);
const existingCooldownMs =
typeof scopeCooldowns[scope] === "string"
? new Date(scopeCooldowns[scope] as string).getTime()
: NaN;
const existingIsAuthoritative =
sourceByScope[scope] === "quota_reset" &&
patch.rateLimitSource !== "quota_reset" &&
Number.isFinite(existingCooldownMs) &&
existingCooldownMs > Date.now();
nextProviderSpecificData.codexScopeRateLimitedUntil = {
...scopeCooldowns,
[scope]: existingIsAuthoritative ? scopeCooldowns[scope] : patch.rateLimitedUntil,
};
nextProviderSpecificData.codexScopeRateLimitSource = {
...sourceByScope,
[scope]: existingIsAuthoritative
if (patch.rateLimitedUntil !== undefined) {
const scopeCooldowns = { ...toRecord(providerSpecificData.codexScopeRateLimitedUntil) };
const sourceByScope = { ...toRecord(providerSpecificData.codexScopeRateLimitSource) };
if (patch.rateLimitedUntil) {
const existingCooldownMs =
typeof scopeCooldowns[scope] === "string"
? new Date(scopeCooldowns[scope] as string).getTime()
: NaN;
const existingIsAuthoritative =
sourceByScope[scope] === "quota_reset" &&
patch.rateLimitSource !== "quota_reset" &&
Number.isFinite(existingCooldownMs) &&
existingCooldownMs > Date.now();
scopeCooldowns[scope] = existingIsAuthoritative
? scopeCooldowns[scope]
: patch.rateLimitedUntil;
sourceByScope[scope] = existingIsAuthoritative
? sourceByScope[scope]
: (patch.rateLimitSource ?? "fallback"),
};
: (patch.rateLimitSource ?? "fallback");
} else {
delete scopeCooldowns[scope];
delete sourceByScope[scope];
}
const nextCooldowns = omitEmptyRecord(scopeCooldowns);
const nextSources = omitEmptyRecord(sourceByScope);
if (nextCooldowns) nextProviderSpecificData.codexScopeRateLimitedUntil = nextCooldowns;
else delete nextProviderSpecificData.codexScopeRateLimitedUntil;
if (nextSources) nextProviderSpecificData.codexScopeRateLimitSource = nextSources;
else delete nextProviderSpecificData.codexScopeRateLimitSource;
}
db.prepare(
@@ -106,6 +208,107 @@ export async function updateCodexScopedQuotaState(
return persisted;
}
/** Grace window absorbing clock skew against the upstream quota server. */
const QUOTA_RESET_CLOCK_SKEW_GRACE_MS = 30_000;
/** Cheap probe: does this connection+scope currently carry a child cooldown? */
export function hasCodexScopeCooldown(id: string, scope: "codex" | "spark"): boolean {
if (typeof id !== "string" || id.length === 0) return false;
const db = getDbInstance() as unknown as DbLike;
const row = db
.prepare("SELECT provider, provider_specific_data FROM provider_connections WHERE id = ?")
.get(id);
if (!row) return false;
const record = toRecord(rowToCamel(row));
if (record.provider !== "codex") return false;
const psd = toRecord(record.providerSpecificData);
return Boolean(toRecord(psd.codexScopeRateLimitedUntil)[scope]);
}
/**
* #12860: When fresh quota snapshot data demonstrates headroom on a scope,
* lift any fallback-sourced cooldown (e.g. parked by quota preflight).
* Cooldowns sourced from upstream 429 quota_reset retain their authority
* until their reset timestamp has elapsed.
*/
export function liftCodexScopeCooldownOnHeadroom(
id: string,
scope: "codex" | "spark"
): boolean {
if (typeof id !== "string" || id.length === 0) return false;
const db = getDbInstance() as unknown as DbLike;
// Cheap eligibility probe so the backup only runs when a write is plausible.
// The transaction below re-reads under lock and remains the authority.
if (!hasCodexScopeCooldown(id, scope)) return false;
backupDbFile("pre-write");
const wrote = db.transaction(() => {
const existing = db
.prepare(
"SELECT provider, provider_specific_data FROM provider_connections WHERE id = ?"
)
.get(id);
if (!existing) return false;
const existingRecord = toRecord(rowToCamel(existing));
if (existingRecord.provider !== "codex") return false;
const currentPsd = toRecord(existingRecord.providerSpecificData);
const currentCooldowns = { ...toRecord(currentPsd.codexScopeRateLimitedUntil) };
if (!currentCooldowns[scope]) return false;
const currentSources = { ...toRecord(currentPsd.codexScopeRateLimitSource) };
const curSource = currentSources[scope];
const curUntilMs =
typeof currentCooldowns[scope] === "string"
? new Date(currentCooldowns[scope] as string).getTime()
: NaN;
// An upstream-authoritative `quota_reset` deadline outranks a local snapshot:
// the grace window absorbs clock skew against the quota server, so a fast
// local clock cannot lift a cooldown upstream still considers active.
if (
curSource === "quota_reset" &&
Number.isFinite(curUntilMs) &&
curUntilMs > Date.now() - QUOTA_RESET_CLOCK_SKEW_GRACE_MS
) {
return false;
}
delete currentCooldowns[scope];
delete currentSources[scope];
const nextPsd: JsonRecord = { ...currentPsd };
const nextCooldowns = omitEmptyRecord(currentCooldowns);
const nextSources = omitEmptyRecord(currentSources);
if (nextCooldowns) nextPsd.codexScopeRateLimitedUntil = nextCooldowns;
else delete nextPsd.codexScopeRateLimitedUntil;
if (nextSources) nextPsd.codexScopeRateLimitSource = nextSources;
else delete nextPsd.codexScopeRateLimitSource;
const exhaustedByScope = { ...toRecord(currentPsd.codexExhaustedWindowByScope) };
if (exhaustedByScope[scope]) {
delete exhaustedByScope[scope];
const nextExhausted = omitEmptyRecord(exhaustedByScope);
if (nextExhausted) {
nextPsd.codexExhaustedWindowByScope = nextExhausted;
} else {
delete nextPsd.codexExhaustedWindowByScope;
delete nextPsd.codexExhaustedWindow;
}
}
const now = new Date().toISOString();
db.prepare(
`UPDATE provider_connections
SET provider_specific_data = ?, updated_at = ?
WHERE id = ?`
).run(JSON.stringify(nextPsd), now, id);
return true;
})();
if (wrote) invalidateDbCache("connections");
return wrote;
}
/** Persist one child cooldown through the shared scoped quota-state transaction. */
export async function updateCodexScopeCooldown(
id: string,

View File

@@ -4,6 +4,9 @@
import { getDbInstance } from "../core";
import { invalidateDbCache } from "../readCache";
import { backupDbFile } from "../backup";
import { bumpProxyConfigGeneration } from "../settings";
import { stripCodexChildCooldownsFromConnection } from "./codexAccountState";
interface StatementLike<TRow = unknown> {
all: (...params: unknown[]) => TRow[];
@@ -28,6 +31,15 @@ interface DbLike {
* @param until - Epoch ms when the rate limit expires (null to clear)
*/
export function setConnectionRateLimitUntil(connectionId: string, until: number | null): void {
// Guard: never persist a non-finite or already-expired timestamp. The TEXT
// column would store "NaN"/"Infinity" and pollute every future read. null
// is the only clear path (via clearConnectionRateLimit); past/zero
// timestamps are noops so an expired write cannot overwrite a live row.
if (until !== null && (!Number.isFinite(until) || until <= Date.now())) return;
if (until == null) {
stripCodexChildCooldownsFromConnection(connectionId, { alsoClearTopLevel: true });
return;
}
const db = getDbInstance() as unknown as DbLike;
db.prepare(
"UPDATE provider_connections SET rate_limited_until = ?, updated_at = ? WHERE id = ?"
@@ -229,6 +241,72 @@ export function clearStaleCrashCooldowns(): { cleared: number } {
return { cleared: toReset.length };
}
/**
* Atomic conditional clear of recoverable error state on a connection row.
*
* Returns true when the row was cleared, false when a concurrent writer
* (markAccountUnavailable, connectionRecovery tick, test, etc.) changed the
* row between the caller's snapshot read and this UPDATE — in which case the
* clear is skipped to preserve the freshest error state. Closes the TOCTOU
* window in the quota-recovery path.
*
* CAS token = (test_status, last_error_at, rate_limited_until).
* Nested Codex child cooldown maps are stripped in the same UPDATE so a
* concurrent writer cannot re-persist them between two statements.
*/
export async function clearConnectionErrorIfUnchanged(
id: string,
expected: {
testStatus: string | null | undefined;
lastErrorAt: string | null | undefined;
rateLimitedUntil: string | null | undefined;
}
): Promise<boolean> {
const db = getDbInstance() as unknown as DbLike;
backupDbFile("pre-write");
const result = db
.prepare(
`
UPDATE provider_connections SET
test_status = 'active',
last_error = NULL,
last_error_at = NULL,
last_error_type = NULL,
last_error_source = NULL,
error_code = NULL,
rate_limited_until = NULL,
backoff_level = 0,
provider_specific_data = CASE
WHEN provider = 'codex' AND json_valid(provider_specific_data)
THEN json_remove(
provider_specific_data,
'$.codexScopeRateLimitedUntil',
'$.codexScopeRateLimitSource'
)
ELSE provider_specific_data
END,
updated_at = ?
WHERE id = ?
AND IFNULL(test_status, '') = ?
AND IFNULL(last_error_at, '') = ?
AND IFNULL(rate_limited_until, '') = ?
`
)
.run(
new Date().toISOString(),
id,
expected.testStatus ?? "",
expected.lastErrorAt ?? "",
expected.rateLimitedUntil ?? ""
);
const applied = (result.changes ?? 0) > 0;
if (applied) {
invalidateDbCache("connections");
bumpProxyConfigGeneration();
}
return applied;
}
// T13: Format a reset countdown as a human-readable string ("2h 35m" / "4m 30s").
// The implementation lives in the client-safe formatting utils so client
// components (e.g. CoolingConnectionsPanel) can import it without pulling this

View File

@@ -1,5 +1,10 @@
import { getDbInstance, rowToCamel } from "./core";
import type { QuotaSnapshotRow, ProviderUtilizationPoint } from "@/shared/types/utilization";
import {
hasCodexScopeCooldown,
liftCodexScopeCooldownOnHeadroom,
} from "./providers/codexAccountState";
import { isCodexSparkQuotaKey } from "@omniroute/open-sse/config/codexQuotaScopes";
type JsonRecord = Record<string, unknown>;
@@ -45,6 +50,61 @@ export function saveQuotaSnapshot(snapshot: Omit<QuotaSnapshotRow, "id" | "creat
}
throw err;
}
// #12860: When a new snapshot demonstrates headroom on a Codex connection,
// lift any fallback-sourced scope cooldown parked by quota preflight.
maybeLiftCodexCooldownOnHeadroom(snapshot);
}
/**
* `rowToCamel` leaves snake_case keys intact on some driver paths, so accept
* either casing rather than trusting one shape.
*/
type SnapshotShape = Partial<QuotaSnapshotRow> & {
windowKey?: string;
remainingPercentage?: number;
isExhausted?: number;
};
function snapshotHasHeadroom(s: SnapshotShape): boolean {
const pct = s.remainingPercentage ?? s.remaining_percentage ?? 0;
const exhausted = s.isExhausted ?? s.is_exhausted ?? 0;
return pct > 0 && exhausted !== 1;
}
function maybeLiftCodexCooldownOnHeadroom(
snapshot: Omit<QuotaSnapshotRow, "id" | "created_at">
): void {
if (
snapshot.provider?.toLowerCase() !== "codex" ||
typeof snapshot.connection_id !== "string" ||
snapshot.connection_id.length === 0 ||
(snapshot.remaining_percentage ?? 0) <= 0 ||
snapshot.is_exhausted === 1
) {
return;
}
try {
const scope = isCodexSparkQuotaKey(snapshot.window_key) ? "spark" : "codex";
// The scope-wide read is only worth paying for when a cooldown is
// actually parked on this connection+scope; the common case is clean.
if (!hasCodexScopeCooldown(snapshot.connection_id, scope)) return;
const scopeWindows = getLatestQuotaSnapshotsForConnection(snapshot.connection_id).filter(
(s: SnapshotShape) => {
const key = s.windowKey ?? s.window_key;
return scope === "spark" ? isCodexSparkQuotaKey(key) : !isCodexSparkQuotaKey(key);
}
);
// Every window in the scope must be healthy: one exhausted window still
// justifies the cooldown even when a sibling reports full headroom.
if (scopeWindows.length > 0 && scopeWindows.every(snapshotHasHeadroom)) {
liftCodexScopeCooldownOnHeadroom(snapshot.connection_id, scope);
}
} catch (err) {
console.debug("[QuotaSnapshots] Headroom evaluation skipped:", err);
}
}
export function getQuotaSnapshots(opts: {

View File

@@ -0,0 +1,236 @@
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
import { isNextBuildPhase } from "../buildPhase";
import type { SqliteAdapter } from "./adapters/types";
import { registerDbStateResetter } from "./stateReset";
/**
* WAL maintenance owns the periodic `wal_checkpoint(TRUNCATE)` lifecycle that
* used to live inside `core.ts`: interval parsing, the scheduler, and reading
* the pragma result so a busy checkpoint warns instead of logging success.
*/
export type WalCheckpointMode = "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE";
export interface WalCheckpointOutcome {
ok: boolean;
busy: boolean;
skipped: boolean;
logFrames: number | null;
checkpointedFrames: number | null;
error: string | null;
}
export interface WalCheckpointContext {
sqliteFile?: string | null;
isCloud?: boolean;
isBuildPhase?: boolean;
}
export interface WalMaintenanceState {
ticks: number;
busyStreak: number;
busyTotal: number;
lastBusyAt: string | null;
lastOkAt: string | null;
}
const isCloud = typeof globalThis.caches === "object" && globalThis.caches !== null;
const DEFAULT_WAL_TRUNCATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
const RETRY_DELAY_MS = 60_000;
let walTimer: NodeJS.Timeout | null = null;
let retryTimer: NodeJS.Timeout | null = null;
let ticks = 0;
let busyStreak = 0;
let busyTotal = 0;
let lastBusyAt: string | null = null;
let lastOkAt: string | null = null;
function recordBusy(): void {
busyStreak++;
busyTotal++;
lastBusyAt = new Date().toISOString();
}
function recordOk(): void {
busyStreak = 0;
lastOkAt = new Date().toISOString();
}
function toFiniteNumber(value: unknown): number | null {
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
function failOpen(): WalCheckpointOutcome {
return {
ok: true,
busy: false,
skipped: false,
logFrames: null,
checkpointedFrames: null,
error: null,
};
}
function parseCheckpointRow(result: unknown): WalCheckpointOutcome {
const row = Array.isArray(result) ? result[0] : result;
if (row === undefined || row === null) return failOpen();
if (typeof row !== "object") return failOpen();
const record = row as Record<string, unknown>;
const busy = toFiniteNumber(record.busy);
const logFrames = toFiniteNumber(record.log);
const checkpointedFrames = toFiniteNumber(record.checkpointed);
if (busy === null || logFrames === null || checkpointedFrames === null) return failOpen();
return {
ok: busy !== 1,
busy: busy === 1,
skipped: false,
logFrames,
checkpointedFrames,
error: null,
};
}
export function runCheckpointNow(
db: SqliteAdapter,
mode: WalCheckpointMode = "TRUNCATE",
ctx: WalCheckpointContext = {}
): WalCheckpointOutcome {
if (ctx.sqliteFile === null || ctx.isCloud === true || ctx.isBuildPhase === true) {
return {
ok: false,
busy: false,
skipped: true,
logFrames: null,
checkpointedFrames: null,
error: null,
};
}
try {
return parseCheckpointRow(db.pragma(`wal_checkpoint(${mode})`));
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return {
ok: false,
busy: false,
skipped: false,
logFrames: null,
checkpointedFrames: null,
error: message,
};
}
}
export function getWalMaintenanceIntervalMs(env: NodeJS.ProcessEnv = process.env): number {
const rawValue = env.OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS;
if (typeof rawValue === "string" && rawValue.trim().length > 0) {
const parsed = Number(rawValue);
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed;
}
}
return DEFAULT_WAL_TRUNCATE_INTERVAL_MS;
}
export function logCheckpointOutcome(
outcome: WalCheckpointOutcome,
mode: WalCheckpointMode,
streak: number
): void {
if (outcome.skipped) return;
if (outcome.busy) {
console.warn(
`[DB] SQLite WAL checkpoint busy — ${outcome.logFrames} frames pending (streak ${streak})`
);
return;
}
if (!outcome.ok) {
console.warn(
`[DB] SQLite WAL checkpoint failed (${mode}): ${outcome.error ?? "unknown error"}`
);
return;
}
console.log(`[DB] SQLite WAL checkpoint completed (${mode})`);
}
function schedulePassiveRetry(db: SqliteAdapter): void {
if (retryTimer) return;
retryTimer = setTimeout(() => {
retryTimer = null;
try {
if (isCloud || isNextBuildPhase() || isAutomatedTestProcess()) return;
if (!db.open) return;
const outcome = runCheckpointNow(db, "PASSIVE");
if (outcome.skipped) return;
if (outcome.busy) {
recordBusy();
logCheckpointOutcome(outcome, "PASSIVE", busyStreak);
} else if (outcome.ok) {
recordOk();
} else {
logCheckpointOutcome(outcome, "PASSIVE", busyStreak);
}
} catch {
// A periodic retry must never throw into the event loop.
}
}, RETRY_DELAY_MS);
retryTimer.unref?.();
}
export function startWalMaintenance(
db: SqliteAdapter,
sqliteFile: string | null,
env: NodeJS.ProcessEnv = process.env
): void {
stopWalMaintenance();
if (sqliteFile === null || isCloud || isNextBuildPhase() || isAutomatedTestProcess()) return;
const intervalMs = getWalMaintenanceIntervalMs(env);
if (intervalMs <= 0) return;
walTimer = setInterval(() => {
try {
if (!db.open) return;
const outcome = runCheckpointNow(db, "TRUNCATE");
if (outcome.skipped) return;
ticks++;
if (outcome.busy) {
recordBusy();
logCheckpointOutcome(outcome, "TRUNCATE", busyStreak);
schedulePassiveRetry(db);
} else if (outcome.ok) {
recordOk();
} else {
logCheckpointOutcome(outcome, "TRUNCATE", busyStreak);
}
} catch {
// A periodic scheduler must never throw into the event loop.
}
}, intervalMs);
walTimer.unref?.();
}
export function stopWalMaintenance(): void {
if (walTimer) {
clearInterval(walTimer);
walTimer = null;
}
if (retryTimer) {
clearTimeout(retryTimer);
retryTimer = null;
}
ticks = 0;
busyStreak = 0;
busyTotal = 0;
lastBusyAt = null;
lastOkAt = null;
}
export function getWalMaintenanceState(): WalMaintenanceState {
return { ticks, busyStreak, busyTotal, lastBusyAt, lastOkAt };
}
export function __resetForTests(): void {
stopWalMaintenance();
}
registerDbStateResetter(stopWalMaintenance);

View File

@@ -4,6 +4,7 @@ import {
} from "@omniroute/open-sse/services/codexAccount/index.ts";
import type { AdaptiveAdmissionPublicSnapshot } from "@omniroute/open-sse/services/admission/runtime.ts";
import type { PerConnectionAdmissionController } from "@/shared/middleware/chatBodyAdmission";
import type { WalMaintenanceState } from "@/lib/db/walMaintenance";
type JsonRecord = Record<string, unknown>;
@@ -37,6 +38,32 @@ export type ChatAdmissionHealthSummary = {
countCapEnabled: boolean;
};
/**
* WAL maintenance health summary (#12853) — the periodic TRUNCATE lifecycle
* from walMaintenance.ts. Fixed low-cardinality shape, never raw-spread.
*/
export type WalMaintenanceSnapshot = Pick<
WalMaintenanceState,
"ticks" | "busyStreak" | "busyTotal" | "lastBusyAt" | "lastOkAt"
>;
/**
* Explicit allowlisted projection of the WAL maintenance state.
* Copies only the documented scalar fields — no timers, no internals.
*/
export function projectWalMaintenanceSummary(
state: WalMaintenanceState | null | undefined
): WalMaintenanceSnapshot | null {
if (!state || typeof state !== "object") return null;
return {
ticks: state.ticks,
busyStreak: state.busyStreak,
busyTotal: state.busyTotal,
lastBusyAt: state.lastBusyAt,
lastOkAt: state.lastOkAt,
};
}
/**
* Explicit allowlisted projection of the structural admission snapshot.
* Never spreads the snapshot — only the documented low-cardinality fields pass.
@@ -217,6 +244,8 @@ interface BuildHealthPayloadOptions {
adaptiveAdmission?: AdaptiveAdmissionPublicSnapshot | null;
/** #11244: optional structural chat-admission snapshot; projected, never raw-spread. */
chatAdmission?: ChatAdmissionSnapshot | null;
/** #12853: optional WAL maintenance snapshot; projected, never raw-spread. */
walMaintenance?: WalMaintenanceSnapshot | null;
}
function limitMonitors(monitors: QuotaMonitorSnapshot[], maxItems = 8): QuotaMonitorSnapshot[] {
@@ -405,6 +434,7 @@ export function buildHealthPayload({
credentialHealth,
adaptiveAdmission = null,
chatAdmission = null,
walMaintenance = null,
buildSha = null,
}: BuildHealthPayloadOptions) {
const timestamp = new Date().toISOString();
@@ -510,6 +540,9 @@ export function buildHealthPayload({
// #11244: the STRUCTURAL gate (chatBodyAdmission.ts) next to the adaptive one —
// distinct key so clients reading `adaptiveAdmission` are untouched.
chatAdmission: projectChatAdmissionSummary(chatAdmission),
// #12853: WAL maintenance next to the admission gates — additive key,
// nothing existing moves.
walMaintenance: projectWalMaintenanceSummary(walMaintenance),
dedup: {
inflightRequests,
},

View File

@@ -346,6 +346,8 @@ export function sanitizeProviderSpecificDataForResponse(value: unknown): JsonRec
delete sanitized.alibabaConsoleSecToken;
delete sanitized.runtimeKey;
delete sanitized.validationId;
delete sanitized.volcConsoleCookie;
delete sanitized.volcCsrfToken;
// System-managed Codex fingerprint seed: never exposed through the API
// (mirrors sub2api stripping `codex_fingerprint_seed`); the server-side
// partial-update merge keeps it alive without the client round-tripping it.

View File

@@ -118,7 +118,20 @@ export async function detectPlan(
if (!result.ok) {
return { available: false, usage: {}, error: result.error };
}
return { available: true, usage: record(result.json.Result), error: null };
const usage = record(result.json.Result);
if (kind === "coding") {
const quotaUsage = usage.QuotaUsage;
if (!Array.isArray(quotaUsage) || quotaUsage.length === 0) {
return { available: false, usage: {}, error: "No active Coding Plan quota windows" };
}
} else if (kind === "agent") {
const hasFiveHour = Boolean(usage.AFPFiveHour && Object.keys(record(usage.AFPFiveHour)).length > 0);
const hasWeekly = Boolean(usage.AFPWeekly && Object.keys(record(usage.AFPWeekly)).length > 0);
if (!hasFiveHour && !hasWeekly) {
return { available: false, usage: {}, error: "No active Agent Plan quota windows" };
}
}
return { available: true, usage, error: null };
}
function firstApiKeyItem(result: JsonRecord): JsonRecord | null {
@@ -173,16 +186,102 @@ async function fetchRawApiKey(
return { apiKey, id, maskedKey: stringField(item?.Key) || null, error: null };
}
async function upsertConnection(
export interface FindTargetConnectionCriteria {
targetConnectionId?: string;
provider: string;
apiKey?: string;
apiKeyId?: number | null;
defaultName: string;
}
/**
* Pure matcher to find an existing connection to adopt or update during console binding.
*
* Matching precedence:
* 1. targetConnectionId priority match (strictly verified against criteria.provider).
* 2. Exact apiKey match, or volcApiKeyId match (numeric and > 0).
* 3. Canonical default name match (e.g. 'Volcano Ark Coding Plan').
* 4. Intentional fallback: safe adoption for single existing connection under this provider.
* - Design rationale: Operators commonly created custom connections (e.g. named 'main')
* prior to console login. This fallback connects console cookies to that sole instance.
* - Safety boundary: strictly scoped to allConnections.length === 1 so that multiple
* distinct accounts are never silently clobbered.
* 5. When multiple connections exist and none match: returns undefined (triggers new connection creation).
*/
export function findTargetConnection(
allConnections: JsonRecord[],
criteria: FindTargetConnectionCriteria
): JsonRecord | undefined {
// 1. targetConnectionId priority match (with provider affinity check)
if (criteria.targetConnectionId) {
const matched = allConnections.find(
(conn) =>
stringField(conn.id) === criteria.targetConnectionId &&
stringField(conn.provider) === criteria.provider
);
if (matched) return matched;
}
// 2. Match by valid apiKey or valid volcApiKeyId
if (criteria.apiKey) {
const matched = allConnections.find(
(conn) =>
stringField(conn.apiKey) === criteria.apiKey &&
stringField(conn.provider) === criteria.provider
);
if (matched) return matched;
}
if (typeof criteria.apiKeyId === "number" && criteria.apiKeyId > 0) {
const matched = allConnections.find((conn) => {
const psd = record(conn.providerSpecificData);
return (
Number(psd.volcApiKeyId) === criteria.apiKeyId &&
stringField(conn.provider) === criteria.provider
);
});
if (matched) return matched;
}
// 3. Match by canonical default name
const nameMatched = allConnections.find(
(conn) =>
stringField(conn.name) === criteria.defaultName &&
stringField(conn.provider) === criteria.provider
);
if (nameMatched) return nameMatched;
// 4. Safe adoption for single connection scenario under matching provider (e.g. user-named 'main')
if (allConnections.length === 1 && stringField(allConnections[0].provider) === criteria.provider) {
return allConnections[0];
}
// 5. Multiple connections exist and none matched -> do not clobber; return undefined
return undefined;
}
export async function upsertConnection(
kind: PlanKind,
apiKey: string,
cookieHeader: string,
csrfToken: string,
apiKeyId: number | null,
usage: JsonRecord
usage: JsonRecord,
targetConnectionId?: string
) {
const cfg = PLAN_CONFIG[kind];
const allConnections = (await getProviderConnections({ provider: cfg.provider })) as JsonRecord[];
const matched = findTargetConnection(allConnections, {
targetConnectionId,
provider: cfg.provider,
apiKey,
apiKeyId,
defaultName: cfg.name,
});
const existingPsd = matched ? record(matched.providerSpecificData) : {};
const providerSpecificData = {
...existingPsd,
volcConsoleCookie: cookieHeader,
volcCsrfToken: csrfToken,
volcApiKeyId: apiKeyId,
@@ -192,14 +291,10 @@ async function upsertConnection(
autoSync: true,
};
const existing = (await getProviderConnections({ provider: cfg.provider })).find(
(conn: JsonRecord) => stringField(conn.name) === cfg.name
);
if (existing?.id) {
return await updateProviderConnection(stringField(existing.id), {
if (matched?.id) {
return await updateProviderConnection(stringField(matched.id), {
apiKey,
name: cfg.name,
name: stringField(matched.name) || cfg.name,
providerSpecificData,
isActive: true,
testStatus: "active",
@@ -217,7 +312,10 @@ async function upsertConnection(
});
}
export async function bindVolcenginePlansFromConsoleCredentials(credentials: JsonRecord) {
export async function bindVolcenginePlansFromConsoleCredentials(
credentials: JsonRecord,
options?: { targetConnectionId?: string }
) {
const cookieHeader = buildCookieHeader(credentials);
const csrfToken = extractCsrf(credentials, cookieHeader);
if (!cookieHeader || !csrfToken) {
@@ -260,7 +358,8 @@ export async function bindVolcenginePlansFromConsoleCredentials(credentials: Jso
cookieHeader,
csrfToken,
key.id,
detected.usage
detected.usage,
options?.targetConnectionId
);
results.push({
plan: kind,
@@ -277,3 +376,8 @@ export async function bindVolcenginePlansFromConsoleCredentials(credentials: Jso
results,
};
}
export const __testing = {
findTargetConnection,
upsertConnection,
};

View File

@@ -51,6 +51,10 @@ const CACHE_TTL_MS = 30_000; // 30 seconds
const _cache = new Map<string, CacheEntry>();
// Pending miss fetches, keyed like _cache. Concurrent getSaturation calls for
// the same key share the promise instead of firing one upstream read each.
const _inflight = new Map<string, Promise<number>>();
// ---------------------------------------------------------------------------
// Rate-limit header cache (populated by response handlers)
// ---------------------------------------------------------------------------
@@ -259,6 +263,7 @@ function cacheKey(connectionId: string, provider: string, dim: DimensionSpec): s
// Exported for test reset
export function _clearSaturationCache(): void {
_cache.clear();
_inflight.clear();
}
// ---------------------------------------------------------------------------
@@ -537,31 +542,40 @@ export async function getSaturation(
return cached.value;
}
let value = 0;
try {
switch (provider) {
case "codex":
value = await fetchCodexSaturation(connectionId, dim, connection);
break;
case "bailian":
value = await fetchBailianSaturation(connectionId, dim);
break;
case "anthropic":
case "claude":
value = await fetchAnthropicSaturation(connectionId, dim);
break;
default:
value = await fetchGenericSaturation(connectionId, provider);
break;
const pending = _inflight.get(key);
if (pending) return pending;
const task = (async (): Promise<number> => {
let value = 0;
try {
switch (provider) {
case "codex":
value = await fetchCodexSaturation(connectionId, dim, connection);
break;
case "bailian":
value = await fetchBailianSaturation(connectionId, dim);
break;
case "anthropic":
case "claude":
value = await fetchAnthropicSaturation(connectionId, dim);
break;
default:
value = await fetchGenericSaturation(connectionId, provider);
break;
}
} catch (err) {
log.warn(
{ err: (err as Error)?.message, connectionId, provider },
"saturation fetch failed — failing open with 0"
);
value = 0;
}
} catch (err) {
log.warn(
{ err: (err as Error)?.message, connectionId, provider },
"saturation fetch failed — failing open with 0"
);
value = 0;
_cache.set(key, { value, ts: Date.now() });
return value;
})();
_inflight.set(key, task);
try {
return await task;
} finally {
_inflight.delete(key);
}
_cache.set(key, { value, ts: Date.now() });
return value;
}

View File

@@ -32,7 +32,7 @@ type QuotaSnapshotView = {
type ProviderHealth = {
provider: string;
remainingPct: number;
remainingPct: number | null;
isExhausted: boolean;
trend: "improving" | "stable" | "declining";
};
@@ -112,11 +112,12 @@ function calculateGini(values: number[]): number {
return (2 * weightedSum) / (count * sum) - (count + 1) / count;
}
function buildProviderHealth(provider: string, snapshots: QuotaSnapshotRow[]): ProviderHealth {
export function buildProviderHealth(provider: string, snapshots: QuotaSnapshotRow[]): ProviderHealth {
if (snapshots.length === 0) {
return {
provider,
remainingPct: 0,
remainingPct: null,
// stable: no data yet, not exhausted — null pct, not 0
isExhausted: false,
trend: "stable",
};
@@ -186,7 +187,7 @@ function buildProviderHealth(provider: string, snapshots: QuotaSnapshotRow[]): P
return {
provider,
remainingPct: roundNumber(lastAverage),
remainingPct: lastValues.length === 0 ? null : roundNumber(lastAverage),
isExhausted,
trend,
};
@@ -218,10 +219,10 @@ function buildConnectionHealth(
});
const firstRemaining =
(firstSnapshot as unknown as QuotaSnapshotView | undefined)?.remainingPercentage ?? 0;
(firstSnapshot as unknown as QuotaSnapshotView | undefined)?.remainingPercentage ?? null;
const lastRemaining =
(lastSnapshot as unknown as QuotaSnapshotView | undefined)?.remainingPercentage ?? 0;
const delta = lastRemaining - firstRemaining;
(lastSnapshot as unknown as QuotaSnapshotView | undefined)?.remainingPercentage ?? null;
const delta = (lastRemaining ?? 0) - (firstRemaining ?? 0);
let trend: ProviderHealth["trend"] = "stable";
if (delta >= 5) trend = "improving";
@@ -229,7 +230,7 @@ function buildConnectionHealth(
return {
provider: `${provider}:${connectionId}`,
remainingPct: roundNumber(lastRemaining),
remainingPct: lastRemaining === null ? null : roundNumber(lastRemaining),
isExhausted:
(ordered[ordered.length - 1] as unknown as QuotaSnapshotView | undefined)?.isExhausted === 1,
trend,
@@ -317,22 +318,19 @@ function buildPerformance(comboName: string, since: string): ComboHealthMetrics[
};
}
function buildQuotaHealth(providers: string[], since: string): ComboHealthMetrics["quotaHealth"] {
export function buildQuotaHealth(providers: string[], since: string): ComboHealthMetrics["quotaHealth"] {
const providerHealth = providers.map((provider) =>
buildProviderHealth(provider, getQuotaSnapshots({ provider, since }))
);
const worstRemainingPct =
providerHealth.length > 0
? providerHealth.reduce(
(lowest, entry) => Math.min(lowest, entry.remainingPct),
providerHealth[0].remainingPct
)
: 0;
const nonNull = providerHealth
.map((entry) => entry.remainingPct)
.filter((v): v is number => typeof v === "number");
const worst = nonNull.length > 0 ? Math.min(...nonNull) : null;
return {
providers: providerHealth,
worstRemainingPct: roundNumber(worstRemainingPct),
worstRemainingPct: worst === null ? null : roundNumber(worst),
};
}

View File

@@ -58,11 +58,11 @@ export interface ComboHealthMetrics {
quotaHealth: {
providers: Array<{
provider: string;
remainingPct: number;
remainingPct: number | null;
isExhausted: boolean;
trend: "improving" | "stable" | "declining";
}>;
worstRemainingPct: number;
worstRemainingPct: number | null;
};
usageSkew: {
modelDistribution: Array<{

View File

@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import { GET } from "@/app/api/health/route";
import { APP_CONFIG } from "@/shared/constants/appConfig";
test("GET /api/health stays minimal: 200, no version anywhere", async () => {
const res = await GET();
assert.equal(res.status, 200);
assert.equal(res.headers.get("ETag"), null);
assert.equal(res.headers.get("X-OmniRoute-Version"), null);
const body = (await res.json()) as Record<string, unknown>;
assert.equal(body.status, "ok");
assert.ok(typeof body.timestamp === "string");
assert.ok(!("version" in body));
});
test("system/version reads no package.json directly", async () => {
const { readFileSync } = await import("node:fs");
const src = readFileSync(path.join(process.cwd(), "src/app/api/system/version/route.ts"), "utf8");
assert.ok(!src.includes("require("), "direct require(package.json) must be gone");
assert.ok(src.includes("APP_CONFIG.version"));
assert.equal(typeof APP_CONFIG.version, "string");
});

View File

@@ -189,3 +189,63 @@ test("SECTIONS maps every dir to a real living-section heading in the fixture",
assert.ok(CHANGELOG_FIXTURE.includes(heading), `fixture must contain ${heading}`);
}
});
test("insertBullets with a version anchors on THAT section even when [Unreleased] has the same heading", () => {
const withUnreleasedHeading = `# Changelog
## [Unreleased]
### ✨ New Features
- **unreleased**: leftover from an older cycle (#9)
## [3.8.47] — TBD
### ✨ New Features
- **existing feature**: already here (#1 — thanks @a)
### 🐛 Bug Fixes
- **fix(x):** existing fix (#2 — thanks @b)
## [3.8.46] - 2026-07-04
### ✨ New Features
- **old feature**: shipped (#0)
`;
const out = insertBullets(
withUnreleasedHeading,
{ features: [{ text: "- **new**: landed (#4)" }] },
"3.8.47"
);
const unreleased = out.slice(out.indexOf("## [Unreleased]"), out.indexOf("## [3.8.47]"));
const living = out.slice(out.indexOf("## [3.8.47]"), out.indexOf("## [3.8.46]"));
assert.ok(!unreleased.includes("landed (#4)"), "must not land under [Unreleased]");
assert.ok(
living.includes("- **existing feature**: already here (#1 — thanks @a)\n- **new**: landed (#4)")
);
assert.throws(
() => insertBullets(withUnreleasedHeading, { features: [{ text: "- x" }] }, "9.9.9"),
/section "## \[9\.9\.9\]" not found/
);
});
test("aggregate reads the living version from package.json and lands fragments under it", () => {
const root = makeRoot({
fragments: { "features/9-new.md": "- **new**: from a fragment (#9)\n" },
});
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "3.8.47" }));
const before = readFileSync(join(root, "CHANGELOG.md"), "utf8");
const withHeading = before.replace(
"## [Unreleased]\n",
"## [Unreleased]\n\n### ✨ New Features\n\n- stale (#8)\n"
);
writeFileSync(join(root, "CHANGELOG.md"), withHeading);
aggregate({ root });
const after = readFileSync(join(root, "CHANGELOG.md"), "utf8");
const unreleased = after.slice(after.indexOf("## [Unreleased]"), after.indexOf("## [3.8.47]"));
assert.ok(!unreleased.includes("from a fragment (#9)"));
assert.ok(after.slice(after.indexOf("## [3.8.47]")).includes("from a fragment (#9)"));
});

View File

@@ -11,6 +11,7 @@ process.env.API_KEY_SECRET = "test-codex-reset-credits-secret";
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const resetCredits = await import("../../src/lib/usage/codexResetCredits.ts");
const codexAccount = await import("../../open-sse/services/codexAccount/index.ts");
const originalFetch = globalThis.fetch;
type QuotaUsageRecord = Record<string, { used?: unknown } | undefined>;
@@ -45,6 +46,69 @@ test.after(async () => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
async function persistBothChildCooldowns(id: string) {
const codexUntil = new Date(Date.now() + 60_000).toISOString();
const sparkUntil = new Date(Date.now() + 120_000).toISOString();
await codexAccount.persistCodexChildCooldown({
connectionId: id,
model: "gpt-5.5",
rateLimitedUntil: codexUntil,
});
await codexAccount.persistCodexChildCooldown({
connectionId: id,
model: "gpt-5.3-codex-spark",
rateLimitedUntil: sparkUntil,
});
return { codexUntil, sparkUntil };
}
async function readConnection(id: string) {
const connection = await providersDb.getProviderConnectionById(id);
assert.ok(connection);
return connection as Record<string, unknown>;
}
function mockResetThenUsage(opts: {
consumeBody: unknown;
consumeStatus?: number;
usageBody?: unknown;
usageStatus?: number;
}) {
globalThis.fetch = (async (url) => {
const href = String(url);
if (href.endsWith("/rate-limit-reset-credits")) {
return new Response(
JSON.stringify({ credits: [{ id: "credit-123", status: "available" }] }),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
if (href.includes("/rate-limit-reset-credits/consume")) {
return new Response(JSON.stringify(opts.consumeBody), {
status: opts.consumeStatus ?? 200,
headers: { "content-type": "application/json" },
});
}
if (href.includes("/backend-api/wham/usage")) {
if (opts.usageStatus && opts.usageStatus >= 500) {
return new Response("upstream down", { status: opts.usageStatus });
}
return new Response(
JSON.stringify(
opts.usageBody ?? {
plan_type: "plus",
rate_limit: {
primary_window: { used_percent: 0 },
secondary_window: { used_percent: 0 },
},
}
),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
return new Response("unexpected", { status: 500 });
}) as typeof fetch;
}
test("consumeCodexResetCredit fetches a credit id, posts it, then refreshes usage", async () => {
const connection = (await createCodexConnection()) as { id: string };
const calls: Array<{ url: string; init: RequestInit }> = [];
@@ -355,3 +419,92 @@ test("consumeCodexResetCredit rejects non-Codex and missing connections", async
error.code === "codex_provider_required"
);
});
test("#12951 consumeCodexResetCredit lifts fallback Codex child cooldown after healthy usage refresh", async () => {
const connection = (await createCodexConnection()) as { id: string };
const parked = await persistBothChildCooldowns(connection.id);
const before = await readConnection(connection.id);
assert.equal(codexAccount.getCodexChildCooldown(before as never, "gpt-5.5"), parked.codexUntil);
assert.equal(
codexAccount.getCodexChildCooldown(before as never, "gpt-5.3-codex-spark"),
parked.sparkUntil
);
mockResetThenUsage({ consumeBody: { code: "reset" } });
const result = await resetCredits.consumeCodexResetCredit(connection.id, "redeem-button-path");
assert.equal(result.outcome, "reset");
const after = await readConnection(connection.id);
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"), null);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
assert.equal(
codexAccount.getCodexChildCooldown(after as never, "gpt-5.3-codex-spark"),
parked.sparkUntil
);
});
test("#12951 consumeCodexResetCredit alreadyRedeemed still needs healthy usage to lift fallback cooldown", async () => {
const connection = (await createCodexConnection()) as { id: string };
await persistBothChildCooldowns(connection.id);
mockResetThenUsage({
consumeBody: { code: "alreadyRedeemed" },
usageBody: {
plan_type: "plus",
rate_limit: {
primary_window: { used_percent: 100 },
secondary_window: { used_percent: 100 },
},
},
});
const result = await resetCredits.consumeCodexResetCredit(connection.id, "redeem-already");
assert.equal(result.outcome, "alreadyRedeemed");
const after = await readConnection(connection.id);
assert.ok(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"));
assert.ok(codexAccount.getCodexChildCooldown(after as never, "gpt-5.3-codex-spark"));
});
test("#12951 consumeCodexResetCredit failed redeem keeps nested child cooldowns", async () => {
const connection = (await createCodexConnection()) as { id: string };
const parked = await persistBothChildCooldowns(connection.id);
mockResetThenUsage({
consumeBody: { code: "noCredit" },
consumeStatus: 409,
});
await assert.rejects(
() => resetCredits.consumeCodexResetCredit(connection.id, "redeem-fail"),
(error: unknown) =>
error instanceof resetCredits.CodexResetCreditError &&
error.status === 409 &&
error.code === "no_credit"
);
const after = await readConnection(connection.id);
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"), parked.codexUntil);
assert.equal(
codexAccount.getCodexChildCooldown(after as never, "gpt-5.3-codex-spark"),
parked.sparkUntil
);
});
test("#12951 consumeCodexResetCredit incomplete usage refresh keeps nested child cooldowns", async () => {
const connection = (await createCodexConnection()) as { id: string };
const parked = await persistBothChildCooldowns(connection.id);
mockResetThenUsage({ consumeBody: { code: "reset" }, usageStatus: 500 });
const result = await resetCredits.consumeCodexResetCredit(connection.id, "redeem-incomplete");
assert.equal(result.outcome, "reset");
const after = await readConnection(connection.id);
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"), parked.codexUntil);
assert.equal(
codexAccount.getCodexChildCooldown(after as never, "gpt-5.3-codex-spark"),
parked.sparkUntil
);
});

View File

@@ -0,0 +1,449 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-clear-12817-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "codex-clear-12817-secret";
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const rateLimit = await import("../../src/lib/db/providers/rateLimit.ts");
const codexAccount = await import("../../open-sse/services/codexAccount/index.ts");
const quotaSnapshots = await import("../../src/lib/db/quotaSnapshots.ts");
const quotaCache = await import("../../src/domain/quotaCache.ts");
async function resetStorage(): Promise<void> {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
interface SeededConnection {
id: string;
providerSpecificData: Record<string, unknown>;
}
async function seedCodexConnection(): Promise<SeededConnection> {
return providersDb.createProviderConnection({
provider: "codex",
authType: "oauth",
name: "codex-clear-12817",
email: "codex-clear-12817@example.com",
apiKey: "codex-clear-12817-key",
accessToken: "codex-clear-12817-access",
refreshToken: "codex-clear-12817-refresh",
providerSpecificData: {
unrelated: { retained: true },
},
}) as unknown as Promise<SeededConnection>;
}
async function seedGlmConnection(): Promise<SeededConnection> {
return providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: "glm-clear-12817",
apiKey: "glm-clear-12817-key",
providerSpecificData: {
leftover: "keep-me",
},
}) as unknown as Promise<SeededConnection>;
}
async function readConnection(id: string): Promise<Record<string, unknown>> {
const connection = await providersDb.getProviderConnectionById(id);
assert.ok(connection);
return connection as unknown as Record<string, unknown>;
}
function psd(connection: Record<string, unknown>): Record<string, unknown> {
return (connection.providerSpecificData ?? {}) as Record<string, unknown>;
}
function futureIso(ms: number): string {
return new Date(Date.now() + ms).toISOString();
}
function quotaHeaders(resetAt5h: string, resetAt7d: string, usage5h = "10") {
return {
"x-codex-5h-usage": usage5h,
"x-codex-5h-limit": "100",
"x-codex-5h-reset-at": resetAt5h,
"x-codex-7d-usage": "10",
"x-codex-7d-limit": "100",
"x-codex-7d-reset-at": resetAt7d,
};
}
async function persistBothChildCooldowns(id: string): Promise<{
codexUntil: string;
sparkUntil: string;
}> {
const codexUntil = futureIso(60_000);
const sparkUntil = futureIso(120_000);
await codexAccount.persistCodexChildCooldown({
connectionId: id,
model: "gpt-5.5",
rateLimitedUntil: codexUntil,
});
await codexAccount.persistCodexChildCooldown({
connectionId: id,
model: "gpt-5.3-codex-spark",
rateLimitedUntil: sparkUntil,
});
return { codexUntil, sparkUntil };
}
test.beforeEach(resetStorage);
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
test("#12817 PUT rateLimitedUntil:null also drops nested Codex child cooldowns", async () => {
const connection = await seedCodexConnection();
const { sparkUntil } = await persistBothChildCooldowns(connection.id);
await providersDb.updateProviderConnection(connection.id, {
rateLimitedUntil: futureIso(90_000),
});
const before = await readConnection(connection.id);
assert.equal(
codexAccount.getCodexChildCooldown(before as never, "gpt-5.3-codex-spark"),
sparkUntil
);
await providersDb.updateProviderConnection(connection.id, { rateLimitedUntil: null });
const after = await readConnection(connection.id);
const data = psd(after);
assert.equal(after.rateLimitedUntil, undefined);
assert.equal(data.codexScopeRateLimitedUntil, undefined);
assert.equal(data.codexScopeRateLimitSource, undefined);
assert.deepEqual(data.unrelated, { retained: true });
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"), null);
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.3-codex-spark"), null);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
});
test("#12817 PUT rateLimitedUntil:\"\" also drops nested Codex child cooldowns", async () => {
const connection = await seedCodexConnection();
await persistBothChildCooldowns(connection.id);
await providersDb.updateProviderConnection(connection.id, {
rateLimitedUntil: futureIso(90_000),
});
await providersDb.updateProviderConnection(connection.id, { rateLimitedUntil: "" });
const after = await readConnection(connection.id);
const data = psd(after);
assert.equal(after.rateLimitedUntil, undefined);
assert.equal(data.codexScopeRateLimitedUntil, undefined);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
});
test("#12817 clearConnectionRateLimit strips nested Codex child cooldowns", async () => {
const connection = await seedCodexConnection();
await persistBothChildCooldowns(connection.id);
rateLimit.setConnectionRateLimitUntil(connection.id, Date.now() + 90_000);
rateLimit.clearConnectionRateLimit(connection.id);
const after = await readConnection(connection.id);
const data = psd(after);
assert.equal(after.rateLimitedUntil, undefined);
assert.equal(data.codexScopeRateLimitedUntil, undefined);
assert.equal(data.codexScopeRateLimitSource, undefined);
assert.deepEqual(data.unrelated, { retained: true });
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
});
test("#12817 CAS error-clear also strips nested Codex child cooldowns", async () => {
const connection = await seedCodexConnection();
await persistBothChildCooldowns(connection.id);
const until = futureIso(90_000);
await providersDb.updateProviderConnection(connection.id, {
testStatus: "unavailable",
lastError: "429",
lastErrorAt: new Date().toISOString(),
lastErrorType: "rate_limit_exceeded",
rateLimitedUntil: until,
});
const before = await readConnection(connection.id);
const applied = await providersDb.clearConnectionErrorIfUnchanged(connection.id, {
testStatus: (before.testStatus as string) ?? null,
lastErrorAt: (before.lastErrorAt as string) ?? null,
rateLimitedUntil: (before.rateLimitedUntil as string) ?? null,
});
assert.equal(applied, true);
const after = await readConnection(connection.id);
assert.equal(after.testStatus, "active");
assert.equal(psd(after).codexScopeRateLimitedUntil, undefined);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
});
test("#12817 a successful quota observation clears that child's leftover cooldown", async () => {
const connection = await seedCodexConnection();
const reset5h = futureIso(60_000);
const reset7d = futureIso(600_000);
await persistBothChildCooldowns(connection.id);
await codexAccount.persistCodexChildQuotaResponse({
connectionId: connection.id,
model: "gpt-5.5",
headers: quotaHeaders(reset5h, reset7d, "95"),
status: 429,
});
await codexAccount.persistCodexChildQuotaResponse({
connectionId: connection.id,
model: "gpt-5.5",
headers: quotaHeaders(reset5h, reset7d, "10"),
status: 200,
});
const after = await readConnection(connection.id);
const data = psd(after);
const until = data.codexScopeRateLimitedUntil as Record<string, unknown> | undefined;
const exhausted = data.codexExhaustedWindowByScope as Record<string, unknown> | undefined;
assert.equal(until?.codex, undefined);
assert.equal(typeof until?.spark, "string");
assert.equal(exhausted?.codex, undefined);
assert.deepEqual(data.unrelated, { retained: true });
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.3-codex-spark"), true);
});
test("#12817 clearing a non-Codex cooldown leaves providerSpecificData alone", async () => {
const connection = await seedGlmConnection();
await providersDb.updateProviderConnection(connection.id, {
rateLimitedUntil: futureIso(90_000),
});
await providersDb.updateProviderConnection(connection.id, { rateLimitedUntil: null });
const after = await readConnection(connection.id);
assert.equal(after.rateLimitedUntil, undefined);
assert.equal(psd(after).leftover, "keep-me");
});
test("#12860 saveQuotaSnapshot with headroom lifts fallback-sourced scope cooldown", async () => {
const connection = await seedCodexConnection();
await persistBothChildCooldowns(connection.id);
const before = await readConnection(connection.id);
assert.ok(codexAccount.getCodexChildCooldown(before as never, "gpt-5.5"));
quotaSnapshots.saveQuotaSnapshot({
provider: "codex",
connection_id: connection.id,
window_key: "primary",
remaining_percentage: 100,
is_exhausted: 0,
next_reset_at: futureIso(3600_000),
window_duration_ms: 18_000_000,
raw_data: null,
});
const after = await readConnection(connection.id);
const data = psd(after);
const until = data.codexScopeRateLimitedUntil as Record<string, unknown> | undefined;
assert.equal(until?.codex, undefined);
assert.equal(typeof until?.spark, "string");
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"), null);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
});
test("#12860 saveQuotaSnapshot with headroom does NOT lift authoritative quota_reset cooldown", async () => {
const connection = await seedCodexConnection();
const reset5h = futureIso(120_000);
const reset7d = futureIso(600_000);
await codexAccount.persistCodexChildQuotaResponse({
connectionId: connection.id,
model: "gpt-5.5",
headers: quotaHeaders(reset5h, reset7d, "95"),
status: 429,
});
const before = await readConnection(connection.id);
assert.ok(codexAccount.getCodexChildCooldown(before as never, "gpt-5.5"));
quotaSnapshots.saveQuotaSnapshot({
provider: "codex",
connection_id: connection.id,
window_key: "session",
remaining_percentage: 100,
is_exhausted: 0,
next_reset_at: futureIso(3600_000),
window_duration_ms: 18_000_000,
raw_data: null,
});
const after = await readConnection(connection.id);
const data = psd(after);
const until = data.codexScopeRateLimitedUntil as Record<string, unknown> | undefined;
assert.equal(typeof until?.codex, "string");
assert.ok(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"));
});
test("#12860 saveQuotaSnapshot does NOT lift scope cooldown if another window for that scope is still exhausted", async () => {
const connection = await seedCodexConnection();
await codexAccount.persistCodexChildCooldown({
connectionId: connection.id,
model: "gpt-5.5",
rateLimitedUntil: futureIso(120_000),
});
// Weekly window is currently exhausted (0% remaining)
quotaSnapshots.saveQuotaSnapshot({
provider: "codex",
connection_id: connection.id,
window_key: "weekly",
remaining_percentage: 0,
is_exhausted: 1,
next_reset_at: futureIso(7200_000),
window_duration_ms: 604_800_000,
raw_data: null,
});
// Session window reports 100% headroom, but weekly is still exhausted
quotaSnapshots.saveQuotaSnapshot({
provider: "codex",
connection_id: connection.id,
window_key: "session",
remaining_percentage: 100,
is_exhausted: 0,
next_reset_at: futureIso(3600_000),
window_duration_ms: 18_000_000,
raw_data: null,
});
let mid = await readConnection(connection.id);
assert.ok(codexAccount.getCodexChildCooldown(mid as never, "gpt-5.5"));
// Now weekly also recovers to 100% headroom
quotaSnapshots.saveQuotaSnapshot({
provider: "codex",
connection_id: connection.id,
window_key: "weekly",
remaining_percentage: 100,
is_exhausted: 0,
next_reset_at: futureIso(7200_000),
window_duration_ms: 604_800_000,
raw_data: null,
});
let after = await readConnection(connection.id);
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"), null);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
});
test("#12860 saveQuotaSnapshot for spark scope with headroom lifts only spark cooldown", async () => {
const connection = await seedCodexConnection();
await persistBothChildCooldowns(connection.id);
quotaSnapshots.saveQuotaSnapshot({
provider: "codex",
connection_id: connection.id,
window_key: "gpt_5_3_codex_spark_session",
remaining_percentage: 100,
is_exhausted: 0,
next_reset_at: futureIso(3600_000),
window_duration_ms: 18_000_000,
raw_data: null,
});
const after = await readConnection(connection.id);
const data = psd(after);
const until = data.codexScopeRateLimitedUntil as Record<string, unknown> | undefined;
assert.equal(typeof until?.codex, "string");
assert.equal(until?.spark, undefined);
assert.ok(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"));
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.3-codex-spark"), null);
});
test("#12860 setQuotaCache with fresh usage headroom lifts fallback-sourced scope cooldown", async () => {
const connection = await seedCodexConnection();
await persistBothChildCooldowns(connection.id);
const before = await readConnection(connection.id);
assert.ok(codexAccount.getCodexChildCooldown(before as never, "gpt-5.5"));
quotaCache.setQuotaCache(connection.id, "codex", {
session: {
used: 0,
total: 100,
remainingPercentage: 100,
resetAt: futureIso(3600_000),
},
});
const after = await readConnection(connection.id);
const data = psd(after);
const until = data.codexScopeRateLimitedUntil as Record<string, unknown> | undefined;
assert.equal(until?.codex, undefined);
assert.equal(typeof until?.spark, "string");
assert.equal(codexAccount.getCodexChildCooldown(after as never, "gpt-5.5"), null);
assert.equal(codexAccount.isCodexChildUnavailable(after as never, "gpt-5.5"), false);
});
test("#12860 quota_reset cooldown that just elapsed is still held by the skew grace window", async () => {
const connection = await seedCodexConnection();
// Reset deadline sits 5s in the past — inside the 30s clock-skew grace, so a
// fast local clock must not lift an upstream-authoritative cooldown early.
const justElapsed = new Date(Date.now() - 5_000).toISOString();
await providersDb.updateProviderConnection(connection.id, {
providerSpecificData: {
codexScopeRateLimitedUntil: { codex: justElapsed },
codexScopeRateLimitSource: { codex: "quota_reset" },
},
});
const lifted = providersDb.liftCodexScopeCooldownOnHeadroom(connection.id, "codex");
assert.equal(lifted, false);
const after = await readConnection(connection.id);
const until = psd(after).codexScopeRateLimitedUntil as Record<string, unknown> | undefined;
assert.equal(until?.codex, justElapsed);
});
test("#12860 quota_reset cooldown past the skew grace window is lifted", async () => {
const connection = await seedCodexConnection();
const wellElapsed = new Date(Date.now() - 120_000).toISOString();
await providersDb.updateProviderConnection(connection.id, {
providerSpecificData: {
codexScopeRateLimitedUntil: { codex: wellElapsed },
codexScopeRateLimitSource: { codex: "quota_reset" },
},
});
const lifted = providersDb.liftCodexScopeCooldownOnHeadroom(connection.id, "codex");
assert.equal(lifted, true);
const after = await readConnection(connection.id);
assert.equal(psd(after).codexScopeRateLimitedUntil, undefined);
});
test("#12860 hasCodexScopeCooldown short-circuits the snapshot scan when nothing is parked", async () => {
const connection = await seedCodexConnection();
assert.equal(providersDb.hasCodexScopeCooldown(connection.id, "codex"), false);
await codexAccount.persistCodexChildCooldown({
connectionId: connection.id,
model: "gpt-5.5",
rateLimitedUntil: futureIso(120_000),
});
assert.equal(providersDb.hasCodexScopeCooldown(connection.id, "codex"), true);
assert.equal(providersDb.hasCodexScopeCooldown(connection.id, "spark"), false);
const glm = await seedGlmConnection();
assert.equal(providersDb.hasCodexScopeCooldown(glm.id, "codex"), false);
assert.equal(providersDb.hasCodexScopeCooldown("does-not-exist", "codex"), false);
});

View File

@@ -0,0 +1,63 @@
// tests/unit/combo-health-empty-snapshot.test.ts — pattern db-quota-snapshots.test.ts:7-26 :
// isolation DB réelle, zéro mock (mock.module indisponible sous tsx/ESM ; sans polyfill+isolateDataDir,
// DATA_DIR tombe sur ~/.omniroute réel → flaky).
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-combo-health-null-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const coreDb = await import("../../src/lib/db/core.ts");
const quotaSnapshotsDb = await import("../../src/lib/db/quotaSnapshots.ts");
const { buildProviderHealth, buildQuotaHealth } =
await import("../../src/lib/usage/comboHealth.ts");
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 SNAP = {
provider: "openrouter",
connection_id: "conn-1",
window_key: "hourly",
is_exhausted: 0,
next_reset_at: "2026-01-01T01:00:00.000Z",
window_duration_ms: 3600000,
raw_data: "{}",
} as const;
test("empty snapshots read null, not 0%, and stay non-exhausted", () => {
const h = buildProviderHealth("openrouter", []);
assert.equal(h.remainingPct, null);
assert.equal(h.isExhausted, false);
assert.equal(h.trend, "stable");
});
test("all-null percentages read null (B3)", () => {
quotaSnapshotsDb.saveQuotaSnapshot({ ...SNAP, remaining_percentage: null });
quotaSnapshotsDb.saveQuotaSnapshot({ ...SNAP, remaining_percentage: null });
const q = buildQuotaHealth(["openrouter"], "1970-01-01T00:00:00.000Z");
assert.equal(q.providers[0].remainingPct, null);
assert.equal(q.worstRemainingPct, null);
});
test("worstRemainingPct ignores nulls, null when all null", () => {
quotaSnapshotsDb.saveQuotaSnapshot({ ...SNAP, remaining_percentage: null });
quotaSnapshotsDb.saveQuotaSnapshot({ ...SNAP, remaining_percentage: 42.346 });
const q = buildQuotaHealth(["openrouter"], "1970-01-01T00:00:00.000Z");
assert.equal(q.worstRemainingPct, 42.35);
});

View File

@@ -0,0 +1,121 @@
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(), "omniroute-db-ratelimit-guard-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts") as typeof import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts") as typeof import("../../src/lib/db/providers.ts");
const {
setConnectionRateLimitUntil,
clearConnectionRateLimit,
isConnectionRateLimited,
} = providersDb;
function readRateLimitedUntil(connectionId: string): unknown {
const db = (
core as unknown as {
getDbInstance: () => {
prepare: (sql: string) => {
get: (id: string) => { rate_limited_until: unknown } | undefined;
};
};
}
).getDbInstance();
return db
.prepare("SELECT rate_limited_until FROM provider_connections WHERE id = ?")
.get(connectionId)?.rate_limited_until ?? null;
}
async function resetStorage() {
core.resetDbInstance();
// Retry loop copied from db-providers-crud.test.ts:17-30 (Windows EBUSY/EPERM).
for (let attempt = 0; attempt < 10; attempt++) {
try {
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
break;
} catch (error: unknown) {
const code = (error as { code?: string } | null)?.code;
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
} else {
throw error;
}
}
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
// No retry here (unlike resetStorage): teardown-only, Linux CI; Windows
// EBUSY surfaces in beforeEach retries, not here.
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
async function seedConnection(): Promise<string> {
const connection = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "Guard probe",
apiKey: "guard-key",
});
return (connection as { id: string }).id;
}
test("ignores NaN (row unchanged)", async () => {
const id = await seedConnection();
setConnectionRateLimitUntil(id, NaN);
assert.equal(readRateLimitedUntil(id), null);
});
test("ignores Infinity and -Infinity", async () => {
const id = await seedConnection();
setConnectionRateLimitUntil(id, Infinity);
setConnectionRateLimitUntil(id, -Infinity);
assert.equal(readRateLimitedUntil(id), null);
});
test("ignores past timestamps and 0 (documented noop; clear path is null)", async () => {
const id = await seedConnection();
setConnectionRateLimitUntil(id, Date.now() - 1000);
setConnectionRateLimitUntil(id, 0);
assert.equal(readRateLimitedUntil(id), null);
});
test("writes future timestamps", async () => {
const id = await seedConnection();
const until = Date.now() + 60_000;
setConnectionRateLimitUntil(id, until);
assert.equal(Number(readRateLimitedUntil(id)), until);
assert.equal(isConnectionRateLimited(id), true);
});
test("preserves null clear (non-regression for clearConnectionRateLimit)", async () => {
const id = await seedConnection();
setConnectionRateLimitUntil(id, Date.now() + 60_000);
clearConnectionRateLimit(id);
assert.equal(readRateLimitedUntil(id), null);
assert.equal(isConnectionRateLimited(id), false);
});
test("expired write does not overwrite a live row (future preserved)", async () => {
const id = await seedConnection();
const future = Date.now() + 60_000;
setConnectionRateLimitUntil(id, future);
setConnectionRateLimitUntil(id, Date.now() - 1000);
assert.equal(Number(readRateLimitedUntil(id)), future);
assert.equal(isConnectionRateLimited(id), true);
});
// Without the guard the TEXT column stores the string "NaN" — rejected by
// current readers, but hygiene demands never writing it.

Some files were not shown because too many files have changed in this diff Show More