From 05857018f458f4e79225ad45a5d54ce683639f95 Mon Sep 17 00:00:00 2001 From: Rian Priskanova Date: Sun, 5 Jul 2026 14:18:41 +0700 Subject: [PATCH 001/109] fix(mitm): strip colons from macOS cert fingerprint before keychain match (#6134) (#6204) fix(mitm): strip colons from macOS cert fingerprint before keychain match (#6134). Extracted testable macCertOutputHasFingerprint helper + regression guard. Thanks @rianonehub. Integrated into release/v3.8.45. --- CHANGELOG.md | 2 ++ src/mitm/cert/install.ts | 15 +++++++- tests/unit/mitm-cert-mac-fingerprint.test.ts | 37 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/unit/mitm-cert-mac-fingerprint.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e6201aa7..77a1f6394f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. +- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) + --- ## [3.8.43] — 2026-07-02 diff --git a/src/mitm/cert/install.ts b/src/mitm/cert/install.ts index 7a8506ef38..74dcf62d45 100644 --- a/src/mitm/cert/install.ts +++ b/src/mitm/cert/install.ts @@ -122,6 +122,19 @@ export async function checkCertInstalled(certPath: string): Promise { return checkCertInstalledLinux(certPath); } +/** + * macOS `security find-certificate -a -Z` prints the SHA-1 as a colon-less + * hex string (e.g. `SHA-1 hash: ABCDEF…`), while {@link getCertFingerprint} + * returns a colon-separated one (`AB:CD:EF…`). A raw substring check therefore + * never matched and the cert was reported as not-installed on every run, + * re-prompting for the sudo install. Normalize both sides (strip `:`, + * upper-case) before comparing. + */ +export function macCertOutputHasFingerprint(securityOutput: string, fingerprint: string): boolean { + const normalize = (value: string) => value.replace(/:/g, "").toUpperCase(); + return normalize(securityOutput).includes(normalize(fingerprint)); +} + async function checkCertInstalledMac(certPath: string): Promise { try { const fingerprint = getCertFingerprint(certPath); @@ -131,7 +144,7 @@ async function checkCertInstalledMac(certPath: string): Promise { "-Z", "/Library/Keychains/System.keychain", ]); - return output.toUpperCase().includes(fingerprint); + return macCertOutputHasFingerprint(output, fingerprint); } catch { return false; } diff --git a/tests/unit/mitm-cert-mac-fingerprint.test.ts b/tests/unit/mitm-cert-mac-fingerprint.test.ts new file mode 100644 index 0000000000..5b25a37112 --- /dev/null +++ b/tests/unit/mitm-cert-mac-fingerprint.test.ts @@ -0,0 +1,37 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { macCertOutputHasFingerprint } from "../../src/mitm/cert/install.ts"; + +// Regression for #6204 (#6134): macOS `security find-certificate -a -Z` prints +// the SHA-1 as a colon-less hex string, while getCertFingerprint() returns a +// colon-separated one. The old `output.toUpperCase().includes(fingerprint)` +// check therefore never matched, so the cert was always reported as +// not-installed and the sudo install re-prompted on every run. + +const FINGERPRINT_WITH_COLONS = "AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01"; +// What `security find-certificate -a -Z` actually emits (no colons). +const MAC_SECURITY_OUTPUT = [ + "keychain: /Library/Keychains/System.keychain", + "SHA-1 hash: ABCDEF0123456789ABCDEF0123456789ABCDEF01", + '"labl"="OmniRoute MITM Root CA"', +].join("\n"); + +test("macCertOutputHasFingerprint matches colon-less security output against a colon-separated fingerprint", () => { + assert.equal(macCertOutputHasFingerprint(MAC_SECURITY_OUTPUT, FINGERPRINT_WITH_COLONS), true); +}); + +test("macCertOutputHasFingerprint returns false when the fingerprint is absent", () => { + const other = "SHA-1 hash: 00000000000000000000000000000000000000FF"; + assert.equal(macCertOutputHasFingerprint(other, FINGERPRINT_WITH_COLONS), false); +}); + +test("macCertOutputHasFingerprint is case-insensitive", () => { + const lower = MAC_SECURITY_OUTPUT.toLowerCase(); + assert.equal(macCertOutputHasFingerprint(lower, FINGERPRINT_WITH_COLONS), true); +}); + +test("pre-fix behavior (raw substring incl. colons) would have missed — documents the bug", () => { + // The pre-#6204 check was `output.toUpperCase().includes(fingerprint)`. + assert.equal(MAC_SECURITY_OUTPUT.toUpperCase().includes(FINGERPRINT_WITH_COLONS), false); +}); From 9b986fa220cb7ac7834fe7c65089016b861193b3 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Jul 2026 04:18:56 -0300 Subject: [PATCH 002/109] =?UTF-8?q?docs(architecture):=20sync=20stale=20DB?= =?UTF-8?q?-layer=20counts=20(45+/55=20=E2=86=92=2095+/110+)=20in=20REPOSI?= =?UTF-8?q?TORY=5FMAP,=20db-schema=20diagram=20and=20llm.txt=20(+42=20i18n?= =?UTF-8?q?=20mirrors)=20(#6167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs(architecture): sync stale DB-layer counts (45+/55 → 95+/110+) across REPOSITORY_MAP, db-schema diagram, llm.txt + 42 i18n mirrors (#6167). Docs-only; check:docs-all passes locally on the reconstruction. Reds are pre-existing base-red drift on release/v3.8.45 (dast-smoke #6228; executor-kiro.test.ts eslint anys; changelog/package.json version drift) — none introduced by this PR. Integrated into release/v3.8.45. --- CHANGELOG.md | 2 ++ docs/architecture/REPOSITORY_MAP.md | 2 +- docs/diagrams/db-schema-overview.mmd | 2 +- docs/i18n/ar/llm.txt | 8 ++++---- docs/i18n/az/llm.txt | 8 ++++---- docs/i18n/bg/llm.txt | 8 ++++---- docs/i18n/bn/llm.txt | 8 ++++---- docs/i18n/cs/llm.txt | 8 ++++---- docs/i18n/da/llm.txt | 8 ++++---- docs/i18n/de/llm.txt | 8 ++++---- docs/i18n/es/llm.txt | 8 ++++---- docs/i18n/fa/llm.txt | 8 ++++---- docs/i18n/fi/llm.txt | 8 ++++---- docs/i18n/fr/llm.txt | 8 ++++---- docs/i18n/gu/llm.txt | 8 ++++---- docs/i18n/he/llm.txt | 8 ++++---- docs/i18n/hi/llm.txt | 8 ++++---- docs/i18n/hu/llm.txt | 8 ++++---- docs/i18n/id/llm.txt | 8 ++++---- docs/i18n/in/llm.txt | 8 ++++---- docs/i18n/it/llm.txt | 8 ++++---- docs/i18n/ja/llm.txt | 8 ++++---- docs/i18n/ko/llm.txt | 8 ++++---- docs/i18n/mr/llm.txt | 8 ++++---- docs/i18n/ms/llm.txt | 8 ++++---- docs/i18n/nl/llm.txt | 8 ++++---- docs/i18n/no/llm.txt | 8 ++++---- docs/i18n/phi/llm.txt | 8 ++++---- docs/i18n/pl/llm.txt | 8 ++++---- docs/i18n/pt-BR/llm.txt | 8 ++++---- docs/i18n/pt/llm.txt | 8 ++++---- docs/i18n/ro/llm.txt | 8 ++++---- docs/i18n/ru/llm.txt | 8 ++++---- docs/i18n/sk/llm.txt | 8 ++++---- docs/i18n/sv/llm.txt | 8 ++++---- docs/i18n/sw/llm.txt | 8 ++++---- docs/i18n/ta/llm.txt | 8 ++++---- docs/i18n/te/llm.txt | 8 ++++---- docs/i18n/th/llm.txt | 8 ++++---- docs/i18n/tr/llm.txt | 8 ++++---- docs/i18n/uk-UA/llm.txt | 8 ++++---- docs/i18n/ur/llm.txt | 8 ++++---- docs/i18n/vi/llm.txt | 8 ++++---- docs/i18n/zh-CN/llm.txt | 8 ++++---- docs/i18n/zh-TW/llm.txt | 8 ++++---- llm.txt | 8 ++++---- 46 files changed, 176 insertions(+), 174 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a1f6394f..543fe5429b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -284,6 +284,8 @@ ### 📝 Maintenance +- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) across `REPOSITORY_MAP.md`, the db-schema diagram and `llm.txt` (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167) — thanks @diegosouzapw) + - **docs (architecture):** add `docs/architecture/ROUTER_BACKENDS.md` — an ADR pinning down how the routing engines (`ts` native, `bifrost`, `cliproxy`, `9router`, VibeProxy-compatible) relate to each other along two orthogonal axes (lifecycle: in-process / supervised / external vs. relay selection backend), answering the architecture questions raised in [#5603](https://github.com/diegosouzapw/OmniRoute/issues/5603) (backend interface model, why CLIProxy spawns a process, feature-flag swapping, actionable route-contract errors). The typed router-backend registry the ADR describes lands separately via [#5868](https://github.com/diegosouzapw/OmniRoute/pull/5868). ([#5891](https://github.com/diegosouzapw/OmniRoute/pull/5891)) - **tests (autoCombo):** stabilize the `getTaskFitnessWithSource identifies fitness_table as source for known models` unit test, which flaked whenever the models.dev capabilities DB was populated in CI: the fixture model `gpt-4o` is a real models.dev catalog id, so the fitness resolution chain returned `models_dev_tier` instead of the expected static `fitness_table` source. The fixture now uses `claude-sonnet` (a shortened alias absent from the models.dev catalog, matching the sibling resolution-chain test), which deterministically falls through to the static table — the exact `source` and score assertions are preserved (`0.95` = `FITNESS_TABLE.coding["claude-sonnet"]`). ([#5890](https://github.com/diegosouzapw/OmniRoute/pull/5890)) — thanks @KooshaPari diff --git a/docs/architecture/REPOSITORY_MAP.md b/docs/architecture/REPOSITORY_MAP.md index 05b4488e14..2b1c5676d0 100644 --- a/docs/architecture/REPOSITORY_MAP.md +++ b/docs/architecture/REPOSITORY_MAP.md @@ -182,7 +182,7 @@ src/ | `compliance/` | Audit log + provider audit — see `docs/security/COMPLIANCE.md` | | `compression/` | Compression engine glue (engines live in `open-sse/services/compression/`) | | `config/` | Runtime config helpers | -| `db/` | 45+ domain DB modules + 55 migrations (always go through here for SQLite) | +| `db/` | 95+ domain DB modules + 110+ migrations (always go through here for SQLite) | | `quota/` | Quota Sharing Engine: `dimensions.ts` (types/Zod), `types.ts` (QuotaStore interface), `sqliteQuotaStore.ts`, `redisQuotaStore.ts`, `storeFactory.ts`, `fairShare.ts`, `burnRate.ts`, `planResolver.ts`, `planRegistry.ts`, `saturationSignals.ts`, `enforce.ts`, `spendRecorder.ts` — see `docs/routing/QUOTA_SHARE.md` | | `display/` | UI formatting helpers (cost, latency, etc.) | | `embeddings/` | Embeddings service helpers | diff --git a/docs/diagrams/db-schema-overview.mmd b/docs/diagrams/db-schema-overview.mmd index debf71f3b1..b5554eccf8 100644 --- a/docs/diagrams/db-schema-overview.mmd +++ b/docs/diagrams/db-schema-overview.mmd @@ -1,5 +1,5 @@ %% Database schema overview (selected core tables) -%% Reflects: src/lib/db/* (45+ modules, 55 migrations) +%% Reflects: src/lib/db/* (95+ modules, 110+ migrations) %% v3.8.0 erDiagram api_keys ||--o{ api_key_usage : tracks diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 4fa9b21284..775cd2b66e 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index 3950dcb984..45c98fe7db 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index 3950dcb984..45c98fe7db 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index 40552a5a11..fdb0178a80 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index ce6de2f065..baf7b823d8 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index d9b36886d6..819865a7ed 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index 80f08179c9..fd8b2bb12c 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index a1cb33aff2..dc3ca451f0 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 6ba4136624..6848cd28f9 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 7c5e76fec8..96224f8a4d 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index ead67c7773..7de5c69aac 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index 65877bf751..3123e0b0aa 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index 8f0e22ab34..f73b2bb1cc 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index ac3b0774ea..64c9880884 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 2f6ad261a3..9f574945c0 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index 4a31546f56..e05008dedb 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 6eee7c1d9a..6e91c869fd 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index 5ab3252731..6db7b5f06a 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 6ce95eddb2..ab529087fe 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 8a8b338b57..8c0aeb75c6 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 7536b91276..cde497ddc3 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index c33b36f427..daf937c1f3 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index dcad1b15b1..38c2c89661 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index 79b1d8c5dd..1d19c59c96 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index dbb53b1aca..783034fdfb 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index 5504d9e4d4..1072ad9f24 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index bedcfae6e2..c117a31086 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index c9cdf06e64..0624b72f3b 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index d34991f191..2810dae83d 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index 6065228f78..923ed9e785 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index 35d7392e29..bf2646ae46 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 885a484dfd..9415c9010d 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index 106571d579..f9ebdc2a3d 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index ef2f4fc981..91f2022328 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 96d1bab868..1de33d1956 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index 89e84411e9..cd0a37688c 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index 32b0be9837..634d4bc259 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index e21defcb6a..14cf3b6600 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index cac8e8c752..c402cff2a1 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index efe5b2c9cd..e21816296c 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 4d3f47bfef..090969a4f0 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index ece4e5b336..891f389de1 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -106,7 +106,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -439,7 +439,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. diff --git a/llm.txt b/llm.txt index 8ba39f8e01..d2995c50a4 100644 --- a/llm.txt +++ b/llm.txt @@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 5.9 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 55 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 110+ 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 @@ -102,7 +102,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ └── streaming.ts # SSE streaming for A2A │ │ ├── acp/ # Agent Communication Protocol registry and manager │ │ ├── compliance/ # Compliance policy engine -│ │ ├── db/ # SQLite database layer (21 modules + migrations) +│ │ ├── db/ # SQLite database layer (95+ modules + migrations) │ │ │ ├── core.ts # Database initialization, connection, schema │ │ │ ├── providers.ts # Provider connection CRUD │ │ │ ├── models.ts # Model catalog management @@ -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/ # 16 SQL migration files +│ │ │ └── migrations/ # 110+ versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -435,7 +435,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo 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 (45+ domain-specific files, 55 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 (95+ domain-specific files, 110+ 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: 75% statements/lines/functions, 70% branches. From 9827ae613797ed311a412dda1e8a3f783c721949 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Jul 2026 04:20:54 -0300 Subject: [PATCH 003/109] fix(api): count tool_use/tool_result/thinking blocks in count_tokens estimate (port from 9router#2337) (#6221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(api): count tool_use/tool_result/thinking blocks in count_tokens estimate (#6221, port from 9router#2337). TDD-covered (6/6); typed the test casts to clear no-new-eslint. Reds are pre-existing base-red drift on release/v3.8.45 (dast-smoke #6228; executor-kiro.test.ts eslint anys; changelog/package.json version drift) — none introduced by this PR. Thanks @luweiCN. Integrated into release/v3.8.45. --- CHANGELOG.md | 2 + src/app/api/v1/messages/count_tokens/route.ts | 63 +++++++++++++- .../unit/messages-count-tokens-route.test.ts | 85 ++++++++++++++++++- 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 543fe5429b..c262fadcf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub) +- **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) + --- ## [3.8.43] — 2026-07-02 diff --git a/src/app/api/v1/messages/count_tokens/route.ts b/src/app/api/v1/messages/count_tokens/route.ts index df37669ba5..6a92bebed5 100644 --- a/src/app/api/v1/messages/count_tokens/route.ts +++ b/src/app/api/v1/messages/count_tokens/route.ts @@ -99,6 +99,59 @@ export async function POST(request) { } } +function safeStringify(value) { + if (typeof value === "string") return value; + try { + return JSON.stringify(value) ?? ""; + } catch { + return ""; + } +} + +// Estimate tokens for a single Anthropic content block. Real agentic +// conversations carry most of their tokens in `tool_use` inputs, `tool_result` +// content, and `thinking` blocks — counting only `text` (as before) reported +// near-zero for those messages and silently broke Claude Code's auto-compaction +// (#2337). Image / redacted_thinking blocks are not text-estimable and count 0. +function estimateContentBlockTokens(part) { + if (!part || typeof part !== "object") return 0; + let tokens = 0; + switch (part.type) { + case "text": + if (typeof part.text === "string") tokens += countTextTokens(part.text); + break; + case "tool_use": + if (typeof part.name === "string") tokens += countTextTokens(part.name); + if (part.input !== undefined) tokens += countTextTokens(safeStringify(part.input)); + break; + case "tool_result": + tokens += estimateToolResultTokens(part.content); + break; + case "thinking": + if (typeof part.thinking === "string") tokens += countTextTokens(part.thinking); + break; + default: + break; + } + return tokens; +} + +// A `tool_result` content can be a plain string or an array of nested blocks +// (text / image). Count string content and nested text blocks. +function estimateToolResultTokens(content) { + if (typeof content === "string") return countTextTokens(content); + if (Array.isArray(content)) { + let tokens = 0; + for (const block of content) { + if (block?.type === "text" && typeof block.text === "string") { + tokens += countTextTokens(block.text); + } + } + return tokens; + } + return 0; +} + function buildEstimatedCountResponse(body) { const messages = Array.isArray(body?.messages) ? body.messages : []; let inputTokens = 0; @@ -111,15 +164,19 @@ function buildEstimatedCountResponse(body) { if (Array.isArray(msg?.content)) { for (const part of msg.content) { - if (part?.type === "text" && typeof part.text === "string") { - inputTokens += countTextTokens(part.text); - } + inputTokens += estimateContentBlockTokens(part); } } } if (typeof body?.system === "string") { inputTokens += countTextTokens(body.system); + } else if (Array.isArray(body?.system)) { + for (const block of body.system) { + if (block?.type === "text" && typeof block.text === "string") { + inputTokens += countTextTokens(block.text); + } + } } return new Response( diff --git a/tests/unit/messages-count-tokens-route.test.ts b/tests/unit/messages-count-tokens-route.test.ts index ffcdcdd461..a066e9cb63 100644 --- a/tests/unit/messages-count-tokens-route.test.ts +++ b/tests/unit/messages-count-tokens-route.test.ts @@ -11,6 +11,13 @@ const core = await import("../../src/lib/db/core.ts"); const providersDb = await import("../../src/lib/db/providers.ts"); const { POST } = await import("../../src/app/api/v1/messages/count_tokens/route.ts"); +type CountTokensResponse = { + input_tokens: number; + source: string; + provider?: string; + model?: string; +}; + async function resetStorage() { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); @@ -68,7 +75,7 @@ test("messages/count_tokens uses real provider count when Claude-compatible upst ); assert.equal(response.status, 200); - const body = (await response.json()) as any; + const body = (await response.json()) as CountTokensResponse; assert.equal(body.input_tokens, 321); assert.equal(body.source, "provider"); assert.equal(body.provider, "anthropic"); @@ -96,7 +103,7 @@ test("messages/count_tokens falls back to estimate when model is missing", async ); assert.equal(response.status, 200); - const body = (await response.json()) as any; + const body = (await response.json()) as CountTokensResponse; assert.equal(body.input_tokens, 4); // tiktoken: "abcd"=1 + "12345678"=3 assert.equal(body.source, "local"); }); @@ -108,11 +115,81 @@ test("count_tokens fallback uses exact tiktoken count with source=local", async body: JSON.stringify({ messages: [{ role: "user", content: "hello world" }] }), }); const res = await POST(req); - const json = (await res.json()) as any; + const json = (await res.json()) as CountTokensResponse; assert.equal(json.source, "local"); assert.equal(json.input_tokens, 2); // exact cl100k_base count, not Math.ceil(11/4)=3 }); +test("count_tokens estimate counts tool_use / tool_result / thinking blocks (not just text) — #2337", async () => { + // Real agentic conversations carry ~95% of their tokens inside tool_use inputs + // and tool_result content. The estimation path used to only sum `text` blocks, + // returning input_tokens: 0 for the shape below, which silently broke Claude + // Code's auto-compaction. Every non-text block below must contribute tokens. + const response = await POST( + new Request("http://localhost/v1/messages/count_tokens", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + messages: [ + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "The user wants me to read a file, let me call the Read tool.", + }, + { + type: "tool_use", + id: "toolu_01", + name: "Read", + input: { file_path: "/tmp/a.txt", limit: 200 }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_01", + content: "line1 line2 line3 some file content here", + }, + ], + }, + ], + }), + }) + ); + + assert.equal(response.status, 200); + const body = (await response.json()) as CountTokensResponse; + assert.equal(body.source, "local"); + // Before the fix this was 0 (only `text` blocks were counted). + assert.ok( + body.input_tokens > 0, + `expected tool/thinking blocks to contribute tokens, got ${body.input_tokens}` + ); +}); + +test("count_tokens estimate counts array-form system prompt blocks — #2337", async () => { + const response = await POST( + new Request("http://localhost/v1/messages/count_tokens", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + system: [{ type: "text", text: "You are a helpful coding assistant." }], + messages: [{ role: "user", content: "hi" }], + }), + }) + ); + + assert.equal(response.status, 200); + const body = (await response.json()) as CountTokensResponse; + assert.equal(body.source, "local"); + // Array-form `system` used to count as 0 (only string system was summed). + assert.ok(body.input_tokens > 1, `expected system blocks counted, got ${body.input_tokens}`); +}); + test("messages/count_tokens falls back to estimate when real upstream count fails", async () => { await seedConnection("anthropic", { apiKey: "sk-ant-fallback" }); @@ -132,7 +209,7 @@ test("messages/count_tokens falls back to estimate when real upstream count fail ); assert.equal(response.status, 200); - const body = (await response.json()) as any; + const body = (await response.json()) as CountTokensResponse; assert.equal(body.input_tokens, 1); // tiktoken: "abcd"=1 assert.equal(body.source, "local"); } finally { From 9ebb53e432506312f103a0dba3850c310050227c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Jul 2026 04:21:38 -0300 Subject: [PATCH 004/109] fix(antigravity): strip trailing assistant prefill turn for Vertex Claude models (#6114) fix(antigravity): strip trailing assistant prefill for Vertex Claude models (#6114). TDD-covered (6/6), merged on TDD strength per owner. Reds are pre-existing base-red drift on release/v3.8.45. Thanks @anki1kr. Integrated into release/v3.8.45. --- CHANGELOG.md | 2 + open-sse/executors/antigravity.ts | 42 +++++++- .../antigravity-claude-prefill-strip.test.ts | 101 ++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 tests/unit/antigravity-claude-prefill-strip.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c262fadcf6..3f467c5c73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - **fix(api):** `/v1/messages/count_tokens` now counts `tool_use`, `tool_result` and `thinking` content blocks (and array-form `system` prompts) in the local-estimation path, instead of only `text`. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard: `tests/unit/messages-count-tokens-route.test.ts`. ([#6221](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN) +- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) + --- ## [3.8.43] — 2026-07-02 diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index a66f58df06..5489e9fbb9 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -480,6 +480,46 @@ function sanitizeAntigravityGeminiRequest( return clean; } +/** + * Ported from decolua/9router#2321 (anki1kr): Vertex AI (used by Antigravity for + * Claude-branded models) rejects a conversation ending on an assistant turn — + * "This model does not support assistant message prefill" — so the request must + * always end on a user turn. Upstream patched `openaiToClaudeRequestForAntigravity` + * (dead code here, zero callers — see `open-sse/translator/request/openai-to-claude.ts`); + * this relocates the same strip to the LIVE Antigravity dispatch path, where Claude + * requests are converted to Gemini `contents` (assistant role is `"model"`, not + * `"assistant"`). Mirrors the trailing-strip pop-loop already used for Mistral + * (#3396), Copilot (#5802), and the CC-bridge in `claudeCodeCompatible.ts`. + * + * Scoped strictly to the Claude path by the caller (`isClaude` branch only) — native + * Gemini models via Antigravity must be unaffected, since Vertex-Claude is the only + * documented rejection surface. + * + * Guard: never strip `contents` down to empty — an empty `contents` array is itself + * an invalid request, so at least one entry (even a lone trailing "model" turn) is + * always preserved. + */ +function stripTrailingAntigravityAssistantTurn( + request: Record +): Record { + const contents = request.contents; + if (!Array.isArray(contents) || contents.length === 0) { + return request; + } + + while ( + contents.length > 1 && + (contents[contents.length - 1] as AntigravityContent)?.role === "model" + ) { + contents.pop(); + } + + return request; +} + +// Test-only export so the unit suite can exercise the strip logic directly. +export const __test_stripTrailingAntigravityAssistantTurn = stripTrailingAntigravityAssistantTurn; + export class AntigravityExecutor extends BaseExecutor { constructor() { super("antigravity", PROVIDERS.antigravity); @@ -660,7 +700,7 @@ export class AntigravityExecutor extends BaseExecutor { }; const transformedRequest = isClaude - ? sanitizeAntigravityGeminiRequest(rawTransformedRequest) + ? stripTrailingAntigravityAssistantTurn(sanitizeAntigravityGeminiRequest(rawTransformedRequest)) : rawTransformedRequest; // Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor") diff --git a/tests/unit/antigravity-claude-prefill-strip.test.ts b/tests/unit/antigravity-claude-prefill-strip.test.ts new file mode 100644 index 0000000000..cac8d881ef --- /dev/null +++ b/tests/unit/antigravity-claude-prefill-strip.test.ts @@ -0,0 +1,101 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { + AntigravityExecutor, + __test_stripTrailingAntigravityAssistantTurn, +} from "../../open-sse/executors/antigravity.ts"; + +/** + * Ports decolua/9router#2321 (anki1kr): Vertex AI (used by Antigravity for + * Claude-branded models) rejects a conversation ending on an assistant turn — + * "This model does not support assistant message prefill" — so the request must + * always end on a user turn. + * + * Upstream's diff patched `openaiToClaudeRequestForAntigravity` in + * `open-sse/translator/request/openai-to-claude.ts`, which has ZERO callers in + * OmniRoute (dead code). The live Antigravity Claude dispatch path converts to + * Gemini `contents` (`role: "user"/"model"`) in `AntigravityExecutor.transformRequest` + * via `sanitizeAntigravityGeminiRequest` — this test drives THAT function end-to-end. + */ + +async function transform(model: string, contents: Array>) { + const executor = new AntigravityExecutor(); + const body = { + request: { + contents, + generationConfig: {}, + }, + }; + const result = await executor.transformRequest(model, body, true, { + projectId: "project-1", + }); + if (result instanceof Response) throw new Error("Unexpected Response from transformRequest"); + return (result as Record).request as Record; +} + +test("(a) strips a single trailing assistant (model) turn for Claude models", async () => { + const request = await transform("antigravity/claude-opus-4-8", [ + { role: "user", parts: [{ text: "Hello" }] }, + { role: "model", parts: [{ text: "Hi there" }] }, // prefill to strip + ]); + const contents = request.contents as Array<{ role: string }>; + assert.equal(contents.length, 1); + assert.equal(contents.at(-1)?.role, "user"); +}); + +test("(b) does NOT strip a trailing model turn for non-Claude (native Gemini) models", async () => { + const request = await transform("antigravity/gemini-3.1-pro", [ + { role: "user", parts: [{ text: "Hello" }] }, + { role: "model", parts: [{ text: "Hi there" }] }, + ]); + const contents = request.contents as Array<{ role: string }>; + assert.equal(contents.length, 2); + assert.equal(contents.at(-1)?.role, "model", "native Gemini requests via Antigravity are untouched"); +}); + +test("(c) a Claude conversation already ending on user is unchanged", async () => { + const request = await transform("antigravity/claude-opus-4-8", [ + { role: "user", parts: [{ text: "Hello" }] }, + { role: "model", parts: [{ text: "Hi" }] }, + { role: "user", parts: [{ text: "What is 2+2?" }] }, + ]); + const contents = request.contents as Array<{ role: string }>; + assert.equal(contents.length, 3); + assert.equal(contents.at(-1)?.role, "user"); +}); + +test("(d) multiple trailing model turns are all stripped", () => { + // Under normal executor flow, adjacent same-role turns are merged before this + // helper runs — this directly exercises the helper's robustness for an input + // that (defensively) still carries multiple consecutive trailing "model" turns. + const request = __test_stripTrailingAntigravityAssistantTurn({ + contents: [ + { role: "user", parts: [{ text: "Hello" }] }, + { role: "model", parts: [{ text: "A" }] }, + { role: "model", parts: [{ text: "B" }] }, + ], + }); + const contents = request.contents as Array<{ role: string }>; + assert.equal(contents.length, 1); + assert.equal(contents.at(-1)?.role, "user"); +}); + +test("(e) never strips contents down to empty", () => { + const request = __test_stripTrailingAntigravityAssistantTurn({ + contents: [{ role: "model", parts: [{ text: "solo prefill" }] }], + }); + const contents = request.contents as Array<{ role: string }>; + // A lone trailing "model" turn is preserved rather than emptying `contents` + // (an empty contents array is itself an invalid upstream request). + assert.equal(contents.length, 1); + assert.equal(contents[0].role, "model"); +}); + +test("empty/missing contents does not throw", () => { + const request = __test_stripTrailingAntigravityAssistantTurn({ contents: [] }); + assert.deepEqual(request.contents, []); + + const request2 = __test_stripTrailingAntigravityAssistantTurn({}); + assert.equal(request2.contents, undefined); +}); From 7e2b8399359d70b00fee81ea142a2e1f26441c0f Mon Sep 17 00:00:00 2001 From: Vittor Guilherme Borges de Oliveira Date: Sun, 5 Jul 2026 05:33:19 -0300 Subject: [PATCH 005/109] fix(security): require management auth for mutable cloud routes (#6233) (#6233) fix(security): require management auth for mutable cloud routes (#6233). Verified: 3 PR tests + full authz/route-guard suite 241/241 green. Thanks @vittoroliveira-dev. Integrated into release/v3.8.45. --- CHANGELOG.md | 2 + src/app/api/cloud/credentials/update/route.ts | 21 +- src/app/api/cloud/models/alias/route.ts | 19 +- src/lib/api/requireManagementAuth.ts | 27 ++- src/server/authz/classify.ts | 10 +- src/shared/constants/publicApiRoutes.ts | 22 +- tests/unit/authz/classify.test.ts | 34 ++- tests/unit/cloud-write-auth.test.ts | 221 ++++++++++++++++++ tests/unit/public-api-routes.test.ts | 10 + 9 files changed, 323 insertions(+), 43 deletions(-) create mode 100644 tests/unit/cloud-write-auth.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f467c5c73..6cc1e627c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/6114)). Regression guard: `tests/unit/antigravity-claude-prefill-strip.test.ts`. (thanks @anki1kr) +- **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) + --- ## [3.8.43] — 2026-07-02 diff --git a/src/app/api/cloud/credentials/update/route.ts b/src/app/api/cloud/credentials/update/route.ts index 25b492e082..ad58f5fbd4 100644 --- a/src/app/api/cloud/credentials/update/route.ts +++ b/src/app/api/cloud/credentials/update/route.ts @@ -1,10 +1,17 @@ import { NextResponse } from "next/server"; -import { validateApiKey, getProviderConnections, updateProviderConnection } from "@/models"; +import { getProviderConnections, updateProviderConnection } from "@/models"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { cloudCredentialUpdateSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; // Update provider credentials (for cloud token refresh) export async function PUT(request: Request) { + const authError = await requireManagementAuth(request, { + alwaysRequireAuth: true, + invalidApiKeyStatus: 401, + }); + if (authError) return authError; + let rawBody; try { rawBody = await request.json(); @@ -16,24 +23,12 @@ export async function PUT(request: Request) { } try { - const authHeader = request.headers.get("Authorization"); - if (!authHeader?.startsWith("Bearer ")) { - return NextResponse.json({ error: "Missing API key" }, { status: 401 }); - } - - const apiKey = authHeader.slice(7); const validation = validateBody(cloudCredentialUpdateSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const { provider, credentials } = validation.data; - // Validate API key - const isValid = await validateApiKey(apiKey); - if (!isValid) { - return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); - } - // Find active connection for provider const connections = await getProviderConnections({ provider, isActive: true }); const connection = connections[0]; diff --git a/src/app/api/cloud/models/alias/route.ts b/src/app/api/cloud/models/alias/route.ts index cbc7a2923f..1f2ae36ad5 100644 --- a/src/app/api/cloud/models/alias/route.ts +++ b/src/app/api/cloud/models/alias/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { validateApiKey, getModelAliases, setModelAlias, isCloudEnabled } from "@/models"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { getConsistentMachineId } from "@/shared/utils/machineId"; import { syncToCloud } from "@/lib/cloudSync"; import { cloudModelAliasUpdateSchema } from "@/shared/validation/schemas"; @@ -7,6 +8,12 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; // PUT /api/cloud/models/alias - Set model alias (for cloud/CLI) export async function PUT(request: Request) { + const authError = await requireManagementAuth(request, { + alwaysRequireAuth: true, + invalidApiKeyStatus: 401, + }); + if (authError) return authError; + let rawBody; try { rawBody = await request.json(); @@ -18,18 +25,6 @@ export async function PUT(request: Request) { } try { - const authHeader = request.headers.get("authorization"); - const apiKey = authHeader?.replace("Bearer ", ""); - - if (!apiKey) { - return NextResponse.json({ error: "Missing API key" }, { status: 401 }); - } - - const isValid = await validateApiKey(apiKey); - if (!isValid) { - return NextResponse.json({ error: "Invalid API key" }, { status: 401 }); - } - const validation = validateBody(cloudModelAliasUpdateSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); diff --git a/src/lib/api/requireManagementAuth.ts b/src/lib/api/requireManagementAuth.ts index 90b4fa7dd5..8540d17c20 100644 --- a/src/lib/api/requireManagementAuth.ts +++ b/src/lib/api/requireManagementAuth.ts @@ -21,8 +21,25 @@ export function hasManageScope(scopes: string[] = []): boolean { return hasManageScopeShared(scopes); } -export async function requireManagementAuth(request: Request): Promise { - if (!(await isAuthRequired(request))) { +interface RequireManagementAuthOptions { + alwaysRequireAuth?: boolean; + invalidApiKeyStatus?: 401 | 403; +} + +function invalidManagementTokenResponse(options: RequireManagementAuthOptions): Response { + const status = options.invalidApiKeyStatus ?? 403; + return createErrorResponse({ + status, + message: status === 401 ? "Invalid API key" : "Invalid management token", + type: "invalid_request", + }); +} + +export async function requireManagementAuth( + request: Request, + options: RequireManagementAuthOptions = {} +): Promise { + if (!options.alwaysRequireAuth && !(await isAuthRequired(request))) { return null; } @@ -73,11 +90,7 @@ export async function requireManagementAuth(request: Request): Promise>; try { if (!(await isValidApiKey(apiKey))) { - return createErrorResponse({ - status: 403, - message: "Invalid management token", - type: "invalid_request", - }); + return invalidManagementTokenResponse(options); } meta = await getApiKeyMetadata(apiKey); } catch { diff --git a/src/server/authz/classify.ts b/src/server/authz/classify.ts index f8fc361565..a5270860d6 100644 --- a/src/server/authz/classify.ts +++ b/src/server/authz/classify.ts @@ -1,7 +1,7 @@ import { - PUBLIC_API_ROUTE_PREFIXES, PUBLIC_READONLY_API_ROUTE_PREFIXES, PUBLIC_READONLY_METHODS, + isPublicApiRoute, } from "../../shared/constants/publicApiRoutes"; import type { ClassificationReason, RouteClassification } from "./types"; @@ -131,11 +131,5 @@ function matchesReadonlyPublic(path: string, method: string): boolean { } function isClassifiedAsPublic(path: string, method: string): boolean { - const isV1ApiPrefix = (p: string) => - p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/"); - const filtered = PUBLIC_API_ROUTE_PREFIXES.filter((p) => p !== "/api/v1/"); - if (filtered.some((prefix) => path.startsWith(prefix)) && !isV1ApiPrefix(path)) { - return true; - } - return matchesReadonlyPublic(path, method); + return isPublicApiRoute(path, method); } diff --git a/src/shared/constants/publicApiRoutes.ts b/src/shared/constants/publicApiRoutes.ts index 26df9df12b..70853e142a 100644 --- a/src/shared/constants/publicApiRoutes.ts +++ b/src/shared/constants/publicApiRoutes.ts @@ -4,7 +4,6 @@ const PUBLIC_API_ROUTE_PREFIXES = [ "/api/auth/status", "/api/init", "/api/v1/", - "/api/cloud/", "/api/sync/bundle", "/api/oauth/", // Public, ticket-gated Codex device-flow completion (validate + persist). @@ -27,7 +26,28 @@ const PUBLIC_READONLY_API_ROUTE_PREFIXES = [ const PUBLIC_READONLY_METHODS = new Set(["GET", "HEAD", "OPTIONS"]); +const PUBLIC_CLOUD_API_ROUTES = [ + { path: "/api/cloud/auth", methods: new Set(["POST", "OPTIONS"]) }, + { path: "/api/cloud/model/resolve", methods: new Set(["POST", "OPTIONS"]) }, + { path: "/api/cloud/models/alias", methods: new Set(["GET", "HEAD", "OPTIONS"]) }, +]; + +function pathMatchesExactRoute(pathname: string, routePath: string): boolean { + return pathname === routePath || pathname === `${routePath}/`; +} + +function isPublicCloudApiRoute(pathname: string, method: string): boolean { + const normalizedMethod = String(method).toUpperCase(); + return PUBLIC_CLOUD_API_ROUTES.some( + ({ path, methods }) => pathMatchesExactRoute(pathname, path) && methods.has(normalizedMethod) + ); +} + export function isPublicApiRoute(pathname: string, method = "GET"): boolean { + if (isPublicCloudApiRoute(pathname, method)) { + return true; + } + if (PUBLIC_API_ROUTE_PREFIXES.some((route) => pathname.startsWith(route))) { return true; } diff --git a/tests/unit/authz/classify.test.ts b/tests/unit/authz/classify.test.ts index c9a76bb950..f091bb2bf9 100644 --- a/tests/unit/authz/classify.test.ts +++ b/tests/unit/authz/classify.test.ts @@ -136,11 +136,41 @@ const cases: Case[] = [ expectedClass: "PUBLIC", }, { - name: "/api/cloud/* is PUBLIC", - path: "/api/cloud/something", + name: "/api/cloud/auth POST is PUBLIC", + path: "/api/cloud/auth", + method: "POST", + expectedClass: "PUBLIC", + }, + { + name: "/api/cloud/model/resolve POST is PUBLIC", + path: "/api/cloud/model/resolve", + method: "POST", + expectedClass: "PUBLIC", + }, + { + name: "/api/cloud/models/alias GET is PUBLIC", + path: "/api/cloud/models/alias", method: "GET", expectedClass: "PUBLIC", }, + { + name: "/api/cloud/credentials/update PUT is MANAGEMENT", + path: "/api/cloud/credentials/update", + method: "PUT", + expectedClass: "MANAGEMENT", + }, + { + name: "/api/cloud/models/alias PUT is MANAGEMENT", + path: "/api/cloud/models/alias", + method: "PUT", + expectedClass: "MANAGEMENT", + }, + { + name: "/api/cloud/unknown GET is MANAGEMENT", + path: "/api/cloud/unknown", + method: "GET", + expectedClass: "MANAGEMENT", + }, { name: "/api/oauth/* is PUBLIC", path: "/api/oauth/callback", diff --git a/tests/unit/cloud-write-auth.test.ts b/tests/unit/cloud-write-auth.test.ts new file mode 100644 index 0000000000..7a04b1e40b --- /dev/null +++ b/tests/unit/cloud-write-auth.test.ts @@ -0,0 +1,221 @@ +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-cloud-write-auth-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.JWT_SECRET = "cloud-write-auth-jwt"; +process.env.INITIAL_PASSWORD = "bootstrap-password"; +process.env.API_KEY_SECRET = "cloud-write-auth-api-key-secret"; + +type ApiKeyRecord = { key: string }; +type ProviderConnectionRecord = { + id: string; + accessToken?: string | null; + refreshToken?: string | null; + expiresAt?: string | null; +}; + +const core = await import("../../src/lib/db/core.ts"); +const localDb = await import("../../src/lib/localDb.ts"); +const credentialsRoute = await import("../../src/app/api/cloud/credentials/update/route.ts"); +const aliasRoute = await import("../../src/app/api/cloud/models/alias/route.ts"); + +async function resetStorage() { + delete process.env.OMNIROUTE_API_KEY; + delete process.env.ROUTER_API_KEY; + process.env.INITIAL_PASSWORD = "bootstrap-password"; + process.env.JWT_SECRET = "cloud-write-auth-jwt"; + process.env.API_KEY_SECRET = "cloud-write-auth-api-key-secret"; + core.resetDbInstance(); + localDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + await localDb.updateSettings({ requireLogin: true, password: "" }); +} + +async function createKey(scopes: string[] = []): Promise { + return localDb.createApiKey(`cloud-write-${scopes.join("-") || "none"}`, "machine-test", scopes); +} + +async function createActiveConnection(): Promise { + const connection = await localDb.createProviderConnection({ + provider: "openai", + authType: "oauth", + name: "OpenAI OAuth", + email: "owner@example.test", + isActive: true, + accessToken: "old-access-token", + refreshToken: "old-refresh-token", + expiresAt: "2026-01-01T00:00:00.000Z", + }); + assert.ok(connection?.id); + return connection as ProviderConnectionRecord; +} + +async function readActiveConnection(): Promise { + const [connection] = (await localDb.getProviderConnections({ + provider: "openai", + isActive: true, + })) as ProviderConnectionRecord[]; + assert.ok(connection); + return connection; +} + +function credentialUpdateBody() { + return { + provider: "openai", + credentials: { + accessToken: "new-access-secret", + refreshToken: "new-refresh-secret", + expiresIn: 3600, + }, + }; +} + +function aliasUpdateBody() { + return { + model: "openai/gpt-4o-mini", + alias: "fast-default", + }; +} + +function cloudCredentialsRequest(token: string | null, body = credentialUpdateBody()) { + const headers = new Headers({ "content-type": "application/json" }); + if (token) headers.set("authorization", `Bearer ${token}`); + return new Request("http://localhost/api/cloud/credentials/update", { + method: "PUT", + headers, + body: JSON.stringify(body), + }); +} + +function cloudAliasRequest(token: string | null, body = aliasUpdateBody()) { + const headers = new Headers({ "content-type": "application/json" }); + if (token) headers.set("authorization", `Bearer ${token}`); + return new Request("http://localhost/api/cloud/models/alias", { + method: "PUT", + headers, + body: JSON.stringify(body), + }); +} + +async function captureConsoleLog(fn: () => Promise): Promise<{ value: T; logs: string }> { + const originalLog = console.log; + const entries: string[] = []; + console.log = (...args: unknown[]) => { + entries.push(args.map((arg) => String(arg)).join(" ")); + }; + try { + return { value: await fn(), logs: entries.join("\n") }; + } finally { + console.log = originalLog; + } +} + +function assertTextDoesNotLeakSecrets(text: string, label: string, secrets: string[]) { + for (const secret of secrets) { + assert.equal(text.includes(secret), false, `${label} leaked secret: ${secret}`); + } +} + +async function assertResponseDoesNotLeakSecrets(response: Response, secrets: string[]) { + const text = await response.text(); + assertTextDoesNotLeakSecrets(text, "response", secrets); + return text.length > 0 ? JSON.parse(text) : null; +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(async () => { + core.resetDbInstance(); + localDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("PUT /api/cloud/credentials/update rejects valid API key without manage/admin scope and leaves credentials unchanged", async () => { + await createActiveConnection(); + const key = await createKey(); + + const { value: response, logs } = await captureConsoleLog(() => + credentialsRoute.PUT(cloudCredentialsRequest(key.key)) + ); + const body = await assertResponseDoesNotLeakSecrets(response, [ + "new-access-secret", + "new-refresh-secret", + ]); + assertTextDoesNotLeakSecrets(logs, "logs", ["new-access-secret", "new-refresh-secret"]); + const connection = await readActiveConnection(); + + assert.equal(response.status, 403); + assert.match(body.error?.message || "", /manage/); + assert.equal(connection.accessToken, "old-access-token"); + assert.equal(connection.refreshToken, "old-refresh-token"); + assert.equal(connection.expiresAt, "2026-01-01T00:00:00.000Z"); +}); + +test("PUT /api/cloud/models/alias rejects valid API key without manage/admin scope and leaves aliases unchanged", async () => { + await localDb.setModelAlias("fast-default", "openai/original-model"); + const key = await createKey(); + + const { value: response, logs } = await captureConsoleLog(() => + aliasRoute.PUT(cloudAliasRequest(key.key)) + ); + const body = await assertResponseDoesNotLeakSecrets(response, ["openai/gpt-4o-mini"]); + assertTextDoesNotLeakSecrets(logs, "logs", ["openai/gpt-4o-mini"]); + const aliases = await localDb.getModelAliases(); + + assert.equal(response.status, 403); + assert.match(body.error?.message || "", /manage/); + assert.equal(aliases["fast-default"], "openai/original-model"); +}); + +test("PUT /api/cloud/credentials/update accepts API key with manage scope", async () => { + await createActiveConnection(); + const key = await createKey(["manage"]); + + const response = await credentialsRoute.PUT(cloudCredentialsRequest(key.key)); + const body = await response.json(); + const connection = await readActiveConnection(); + + assert.equal(response.status, 200); + assert.equal(body.success, true); + assert.equal(connection.accessToken, "new-access-secret"); + assert.equal(connection.refreshToken, "new-refresh-secret"); + assert.notEqual(connection.expiresAt, "2026-01-01T00:00:00.000Z"); +}); + +test("PUT /api/cloud/models/alias accepts API key with manage scope", async () => { + const key = await createKey(["manage"]); + + const response = await aliasRoute.PUT(cloudAliasRequest(key.key)); + const body = await response.json(); + const aliases = await localDb.getModelAliases(); + + assert.equal(response.status, 200); + assert.equal(body.success, true); + assert.equal(aliases["fast-default"], "openai/gpt-4o-mini"); +}); + +test("cloud write routes keep 401 for missing or invalid Bearer credentials", async () => { + await createActiveConnection(); + + const { value: missing, logs: missingLogs } = await captureConsoleLog(() => + credentialsRoute.PUT(cloudCredentialsRequest(null)) + ); + const { value: invalid, logs: invalidLogs } = await captureConsoleLog(() => + aliasRoute.PUT(cloudAliasRequest("sk-invalid")) + ); + await assertResponseDoesNotLeakSecrets(missing, ["new-access-secret", "new-refresh-secret"]); + await assertResponseDoesNotLeakSecrets(invalid, ["openai/gpt-4o-mini"]); + assertTextDoesNotLeakSecrets(missingLogs, "logs", ["new-access-secret", "new-refresh-secret"]); + assertTextDoesNotLeakSecrets(invalidLogs, "logs", ["openai/gpt-4o-mini"]); + + assert.equal(missing.status, 401); + assert.equal(invalid.status, 401); +}); diff --git a/tests/unit/public-api-routes.test.ts b/tests/unit/public-api-routes.test.ts index 00b2b02ecf..e480c528aa 100644 --- a/tests/unit/public-api-routes.test.ts +++ b/tests/unit/public-api-routes.test.ts @@ -9,6 +9,16 @@ test("isPublicApiRoute allows public management prefixes", () => { assert.equal(isPublicApiRoute("/api/oauth/cursor/callback"), true); }); +test("isPublicApiRoute keeps cloud read/auth routes public but not cloud write routes", () => { + assert.equal(isPublicApiRoute("/api/cloud/auth", "POST"), true); + assert.equal(isPublicApiRoute("/api/cloud/model/resolve", "POST"), true); + assert.equal(isPublicApiRoute("/api/cloud/models/alias", "GET"), true); + + assert.equal(isPublicApiRoute("/api/cloud/credentials/update", "PUT"), false); + assert.equal(isPublicApiRoute("/api/cloud/models/alias", "PUT"), false); + assert.equal(isPublicApiRoute("/api/cloud/unknown", "GET"), false); +}); + test("isPublicApiRoute allows readonly health and require-login bootstrap routes", () => { assert.equal(isPublicApiRoute("/api/monitoring/health", "GET"), true); assert.equal(isPublicApiRoute("/api/monitoring/health", "HEAD"), true); From 0e0ca7e26be3eab7a786a483d71a5740d533897a Mon Sep 17 00:00:00 2001 From: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:37:42 -0700 Subject: [PATCH 006/109] fix(dashboard): use connection.id (UUID) not connection.provider (category) in onboarding wizard href (issue #6144) (#6166) refactor(dashboard): extract tested buildProviderDetailsHref helper for onboarding wizard (#6166). Behavioral #6144 fix already on tip via #6145; this lands the tested-helper hardening. Thanks @KooshaPari. Integrated into release/v3.8.45. --- CHANGELOG.md | 2 + .../onboarding/ProviderOnboardingWizard.tsx | 22 +++++++---- .../onboarding/providerOnboardingHref.ts | 33 +++++++++++++++++ tests/unit/provider-onboarding-href.test.ts | 37 +++++++++++++++++++ 4 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingHref.ts create mode 100644 tests/unit/provider-onboarding-href.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc1e627c2..bfa214eb1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - **fix(security):** the mutable cloud-agent routes (`/api/cloud/credentials/update`, `/api/cloud/models/alias`) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated by `requireManagementAuth`; cloud **read**/auth routes stay public. Regression guards: `tests/unit/cloud-write-auth.test.ts`, `tests/unit/authz/classify.test.ts`, `tests/unit/public-api-routes.test.ts`. ([#6233](https://github.com/diegosouzapw/OmniRoute/pull/6233) — thanks @vittoroliveira-dev) +- **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) + --- ## [3.8.43] — 2026-07-02 diff --git a/src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx b/src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx index 65510ef0ce..4a44da1a89 100644 --- a/src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx +++ b/src/app/(dashboard)/dashboard/providers/components/onboarding/ProviderOnboardingWizard.tsx @@ -22,6 +22,7 @@ import { getWizardOAuthProviderOptions, type WizardProviderOption, } from "./providerOnboardingCatalog"; +import { buildProviderDetailsHref } from "./providerOnboardingHref"; import { createCompatibleProviderNode, createOnboardingConnection, @@ -260,14 +261,19 @@ function ResultSummary({ )}
- {connection?.id && ( - - {providerText(t, "onboardingOpenProviderDetails", "Open provider details")} - - )} + {(() => { + const detailsHref = buildProviderDetailsHref(connection); + return ( + detailsHref && ( + + {providerText(t, "onboardingOpenProviderDetails", "Open provider details")} + + ) + ); + })()} & { + provider?: string; +}; + +/** + * Build the "open provider details" link target for the onboarding wizard + * success card. The dashboard detail route is keyed by the connection's + * server-assigned UUID (`OnboardingConnection.id`), not by the provider + * category (`connection.provider` is e.g. "openai-compatible" and is shared + * across many connections). + * + * Returns `null` if no usable id is present so callers can hide the action + * entirely rather than linking to a 404. + */ +export function buildProviderDetailsHref( + connection: HrefConnection | null | undefined +): string | null { + const id = connection?.id?.trim(); + if (!id) return null; + return `/dashboard/providers/${encodeURIComponent(id)}`; +} \ No newline at end of file diff --git a/tests/unit/provider-onboarding-href.test.ts b/tests/unit/provider-onboarding-href.test.ts new file mode 100644 index 0000000000..95548d0cab --- /dev/null +++ b/tests/unit/provider-onboarding-href.test.ts @@ -0,0 +1,37 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildProviderDetailsHref } from "../../src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingHref"; + +test("buildProviderDetailsHref uses the server-assigned connection UUID", () => { + const href = buildProviderDetailsHref({ + id: "9f3c1a4d-1c2b-4a3c-8def-0123456789ab", + }); + assert.equal( + href, + "/dashboard/providers/9f3c1a4d-1c2b-4a3c-8def-0123456789ab" + ); +}); + +test("buildProviderDetailsHref does not leak the provider category into the URL", () => { + // Regression for issue #6144: previously the wizard used + // `connection.provider` ("openai-compatible") as the URL slug, which 404'd + // on /dashboard/providers/[id] because that route is keyed by UUID. + const href = buildProviderDetailsHref({ + id: "9f3c1a4d-1c2b-4a3c-8def-0123456789ab", + provider: "openai-compatible", + }); + assert.notEqual(href, "/dashboard/providers/openai-compatible"); + assert.match(href ?? "", /\/dashboard\/providers\/[0-9a-f-]+$/); +}); + +test("buildProviderDetailsHref returns null when no id is available", () => { + assert.equal(buildProviderDetailsHref(null), null); + assert.equal(buildProviderDetailsHref(undefined), null); + assert.equal(buildProviderDetailsHref({ id: "" }), null); + assert.equal(buildProviderDetailsHref({ id: " " }), null); +}); + +test("buildProviderDetailsHref percent-encodes unusual ids", () => { + const href = buildProviderDetailsHref({ id: "abc/123 def" }); + assert.equal(href, "/dashboard/providers/abc%2F123%20def"); +}); \ No newline at end of file From f26aa16da3ce954239c102edf9a4a4e2adddfe27 Mon Sep 17 00:00:00 2001 From: Milan Soni <123074437+Iammilansoni@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:16:17 +0530 Subject: [PATCH 007/109] feat(rankings): add 'Configured Only' filter to Free Provider Rankings page (#6245) feat(rankings): add 'Configured Only' filter to Free Provider Rankings (#6245, closes #6150). 9/9 test green. Thanks @Iammilansoni. Integrated into release/v3.8.45. --- CHANGELOG.md | 4 + .../dashboard/free-provider-rankings/page.tsx | 76 +++++++++++++-- src/i18n/messages/en.json | 6 +- ...rovider-rankings-configured-filter.test.ts | 93 +++++++++++++++++++ 4 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 tests/unit/free-provider-rankings-configured-filter.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa214eb1c..a4fec7f6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ - **refactor(dashboard):** extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested `buildProviderDetailsHref(connection)` helper. The wizard already routes by `connection.id` (the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard: `tests/unit/provider-onboarding-href.test.ts`. ([#6166](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari) +### ✨ New Features + +- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) + --- ## [3.8.43] — 2026-07-02 diff --git a/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx b/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx index a112f299d5..31a6614076 100644 --- a/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx +++ b/src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx @@ -60,6 +60,8 @@ export default function FreeProviderRankingsPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [filter, setFilter] = useState(""); + const [configuredOnly, setConfiguredOnly] = useState(false); + const [configuredProviderIds, setConfiguredProviderIds] = useState>(new Set()); const fetchRankings = useCallback( async (category?: string) => { @@ -86,6 +88,28 @@ export default function FreeProviderRankingsPage() { fetchRankings(filter || undefined); }, [filter, fetchRankings]); + useEffect(() => { + let active = true; + fetch("/api/providers") + .then((res) => (res.ok ? res.json() : { connections: [] })) + .then((data) => { + if (!active) return; + const ids = new Set(); + for (const conn of data.connections || []) { + if (conn?.provider) ids.add(conn.provider); + } + setConfiguredProviderIds(ids); + }) + .catch(() => {}); + return () => { + active = false; + }; + }, []); + + const displayedRankings = configuredOnly + ? rankings.filter((r) => configuredProviderIds.has(r.id)) + : rankings; + return (
{/* Header */} @@ -111,6 +135,30 @@ export default function FreeProviderRankingsPage() { {t(opt.labelKey)} ))} +
+ + +
{error &&
{error}
} @@ -122,9 +170,9 @@ export default function FreeProviderRankingsPage() { ) : ( <> {/* Top 3 Podium */} - {rankings.length >= 3 && ( + {displayedRankings.length >= 3 && (
- {rankings.slice(0, 3).map((provider, idx) => ( + {displayedRankings.slice(0, 3).map((provider, idx) => (
0 && ( + {displayedRankings.length > 0 && (
@@ -181,10 +229,11 @@ export default function FreeProviderRankingsPage() { + - {rankings.map((provider, idx) => ( + {displayedRankings.map((provider, idx) => ( + ))} @@ -237,9 +297,13 @@ export default function FreeProviderRankingsPage() { )} - {rankings.length === 0 && !error && ( + {displayedRankings.length === 0 && !error && ( -
{t("emptyState")}
+
+ {configuredOnly && rankings.length > 0 + ? t("noConfiguredProviders") + : t("emptyState")} +
)} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 75b632a949..8a34193da8 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -9030,7 +9030,11 @@ "colScore": "Score", "colAvgScore": "Avg Score", "colModels": "Models", - "colType": "Type" + "colType": "Type", + "configuredOnly": "Configured Only", + "configuredOnlyHint": "Show only providers with active connections", + "noConfiguredProviders": "No configured providers found. Add a provider connection first.", + "colConfigured": "Status" }, "discovery": { "title": "Provider Discovery", diff --git a/tests/unit/free-provider-rankings-configured-filter.test.ts b/tests/unit/free-provider-rankings-configured-filter.test.ts new file mode 100644 index 0000000000..86b16552de --- /dev/null +++ b/tests/unit/free-provider-rankings-configured-filter.test.ts @@ -0,0 +1,93 @@ +/** + * Unit tests for the "Configured Only" filter on the Free Provider Rankings page. + * + * Phase 1 of #6150 — verifies the toggle state, filtering logic, status column, + * cleanup flag, and i18n keys exist in the source code. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const root = join(import.meta.dirname, "../.."); +const read = (p: string) => readFileSync(join(root, p), "utf8"); +const pageSrc = read("src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx"); +const en = JSON.parse(read("src/i18n/messages/en.json")); + +test("page declares configuredOnly state", () => { + assert.ok(pageSrc.includes("useState(false)"), "configuredOnly defaults to false"); + assert.ok(pageSrc.includes("setConfiguredOnly"), "setConfiguredOnly setter exists"); +}); + +test("page declares configuredProviderIds state", () => { + assert.ok(pageSrc.includes("configuredProviderIds"), "configuredProviderIds state exists"); + assert.ok(pageSrc.includes("Set"), "configuredProviderIds is typed as Set"); +}); + +test("page fetches /api/providers on mount", () => { + assert.ok(pageSrc.includes('fetch("/api/providers")'), "fetches /api/providers"); + assert.ok(pageSrc.includes("conn?.provider"), "uses optional chaining for conn.provider"); +}); + +test("useEffect has cleanup flag to prevent stale state updates", () => { + assert.ok(pageSrc.includes("let active = true"), "declares cleanup flag"); + assert.ok(pageSrc.includes("if (!active) return"), "guards state update with active flag"); + assert.ok(pageSrc.includes("active = false"), "cleanup function sets active to false"); +}); + +test("displayedRankings filters by configuredProviderIds when toggle is on", () => { + assert.ok(pageSrc.includes("displayedRankings"), "displayedRankings derived variable exists"); + assert.ok( + pageSrc.includes("configuredProviderIds.has(r.id)"), + "filters rankings by configuredProviderIds.has(r.id)" + ); + assert.ok( + pageSrc.includes("configuredOnly\n ? rankings.filter"), + "conditional: when configuredOnly is true, filters rankings" + ); +}); + +test("toggle switch has accessible attributes", () => { + assert.ok(pageSrc.includes('role="switch"'), "toggle has role=switch"); + assert.ok( + pageSrc.includes("aria-checked={configuredOnly}"), + "toggle has aria-checked bound to configuredOnly" + ); + assert.ok( + pageSrc.includes('htmlFor="configured-only-toggle"'), + "label is linked to toggle via htmlFor" + ); +}); + +test("table has a 'Configured' status column", () => { + assert.ok(pageSrc.includes('t("colConfigured")'), "table header includes colConfigured key"); + assert.ok( + pageSrc.includes("configuredProviderIds.has(provider.id)"), + "status column checks configuredProviderIds" + ); +}); + +test("empty state shows noConfiguredProviders when toggle is on", () => { + assert.ok( + pageSrc.includes('t("noConfiguredProviders")'), + "empty state uses noConfiguredProviders i18n key" + ); + assert.ok( + pageSrc.includes("configuredOnly && rankings.length > 0"), + "shows noConfiguredProviders only when toggle is on and data exists" + ); +}); + +test("i18n: en.json has all required filter keys", () => { + const keys = en.freeProviderRankingsPage; + assert.ok(keys, "freeProviderRankingsPage namespace exists in en.json"); + assert.equal(typeof keys.configuredOnly, "string", "configuredOnly is a string"); + assert.equal(typeof keys.configuredOnlyHint, "string", "configuredOnlyHint is a string"); + assert.equal(typeof keys.noConfiguredProviders, "string", "noConfiguredProviders is a string"); + assert.equal(typeof keys.colConfigured, "string", "colConfigured is a string"); + assert.ok(keys.configuredOnly.length > 0, "configuredOnly is non-empty"); + assert.ok(keys.configuredOnlyHint.length > 0, "configuredOnlyHint is non-empty"); + assert.ok(keys.noConfiguredProviders.length > 0, "noConfiguredProviders is non-empty"); + assert.ok(keys.colConfigured.length > 0, "colConfigured is non-empty"); +}); From c347abb7747017182385947221de5ccd44721f42 Mon Sep 17 00:00:00 2001 From: serverless83 <35410475+serverless83@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:48:35 +0200 Subject: [PATCH 008/109] fix(i18n): add 118 missing Italian translations (#6212) i18n(it): add 118 Italian translations (#6212). Audited net-additive (0 keys dropped, valid JSON). Thanks @serverless83. Integrated into release/v3.8.45. --- CHANGELOG.md | 4 + src/i18n/messages/it.json | 154 ++++++++++++++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4fec7f6e9..eeefce72b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ - **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni) +### 📝 Maintenance + +- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/6212) — thanks @serverless83) + --- ## [3.8.43] — 2026-07-02 diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 751b0b4bb8..f10799a667 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -841,7 +841,9 @@ "batchDetailCancelConfirm": "Cancel this batch? In-progress requests will stop.", "batchActionCancelError": "Failed to cancel batch. Try again.", "batchActionRetryError": "Failed to retry failed requests. Try again.", - "batchConceptRetentionNote": "Results and error files are retained for 30 days (Anthropic: 29 days)" + "batchConceptRetentionNote": "Results and error files are retained for 30 days (Anthropic: 29 days)", + "manualConfig": "Configurazione manuale", + "unknownProvider": "Provider sconosciuto" }, "featureFlagOmnirouteEmergencyFallbackDescription": "__MISSING__:Route budget-exhausted requests to the emergency free fallback provider/model.", "featureFlagArenaEloSyncEnabledDescription": "__MISSING__:Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings.", @@ -1114,7 +1116,9 @@ "dragReorderItem": "__MISSING__:Drag to reorder", "cannotHide": "__MISSING__:This item cannot be hidden", "alwaysVisible": "__MISSING__:Always visible", - "groupSeparatorLabel": "__MISSING__:Separator" + "groupSeparatorLabel": "__MISSING__:Separator", + "discovery": "Discovery", + "discoverySubtitle": "Scansiona provider per accesso gratuito" }, "webhooks": { "title": "Webhook", @@ -1806,7 +1810,9 @@ "normalKeysSection": "__MISSING__:Normal keys", "quotaKeysSection": "__MISSING__:Quota keys", "quotaPill": "__MISSING__:QUOTA", - "quotaModeOnly": "__MISSING__:qtSd-only" + "quotaModeOnly": "__MISSING__:qtSd-only", + "devicesCount": "{count, plural, one {# dispositivo} other {# dispositivi}}", + "devicesTooltip": "{count, plural, one {# IP/User-Agent distinto visto con questa chiave (ultimi 30 min)} other {# IP/User-Agent distinti visti con questa chiave (ultimi 30 min)}}" }, "auditLog": { "title": "Registro di controllo", @@ -1853,14 +1859,16 @@ "webSearch": "__MISSING__:Web Search", "webFetch": "__MISSING__:Web Fetch", "video": "__MISSING__:Video", - "music": "__MISSING__:Music" + "music": "__MISSING__:Music", + "ocr": "OCR" }, "noProviders": "__MISSING__:No providers configured for this kind yet.", "addConnection": "__MISSING__:Add Connection", "backToProviders": "__MISSING__:Back to Providers", "connections": "__MISSING__:{count} Connections", "noConnections": "__MISSING__:No connections yet — add one from the provider page.", - "loading": "__MISSING__:Loading..." + "loading": "__MISSING__:Loading...", + "suggestedModels": "Modelli suggeriti dal provider" }, "search": { "searchQuery": "Search Query", @@ -2804,7 +2812,22 @@ "agentFeaturesContextLengthErrorInteger": "La lunghezza del contesto deve essere un numero intero valido", "agentFeaturesContextLengthErrorRange": "La lunghezza del contesto deve essere compresa tra 1.000 e 2.000.000", "compressionOverride": "Sostituzione della compressione", - "modePack": "Pacchetto modalità" + "modePack": "Pacchetto modalità", + "fusionJudgeModel": "Modello giudice", + "fusionJudgeModelHelp": "Modello che sintetizza le risposte del panel in un'unica risposta finale. Lascia vuoto per usare il primo modello del panel.", + "fusionMinPanel": "Panel minimo", + "fusionMinPanelHelp": "Risposte del panel necessarie prima che i ritardatari ricevano una finestra di grazia (default 2).", + "fusionPanelHardTimeoutMs": "Timeout massimo panel (ms)", + "fusionPanelHardTimeoutMsHelp": "Limite assoluto per evitare che un modello bloccato fermi l'intero panel (default 90000).", + "fusionStragglerGraceMs": "Grazia ritardatari (ms)", + "fusionStragglerGraceMsHelp": "Quanto attendere i modelli lenti del panel una volta raggiunto il quorum (default 8000).", + "responseValidationAddCheck": "+ Aggiungi controllo", + "responseValidationForbidden": "Sottostringhe vietate (una per riga)", + "responseValidationHelp": "Passa al prossimo target quando un corpo 200 OK non supera questi controlli (contenuto dell'assistente).", + "responseValidationJsonPaths": "Controlli JSON-path", + "responseValidationMinLength": "Lunghezza minima contenuto (caratteri)", + "responseValidationRequired": "Sottostringhe richieste (una per riga)", + "responseValidationTitle": "Validazione risposta" }, "costs": { "title": "Costi", @@ -3432,7 +3455,11 @@ "cleaning": "__MISSING__:Cleaning...", "cleanNow": "__MISSING__:Clean now", "cleanupSuccess": "__MISSING__:{count} point(s) removed", - "cleanupFailed": "__MISSING__:Cleanup failed" + "cleanupFailed": "__MISSING__:Cleanup failed", + "banner": "Vector store Tier 2 — un'alternativa esterna e scalabile al sqlite-vec integrato (Tier 1). Attivalo solo se hai un insieme di memorie molto grande o vuoi memoria condivisa tra istanze; la maggior parte degli utenti sta bene con sqlite-vec. Quando attivato diventa lo store primario e fa automaticamente fallback a sqlite-vec se non raggiungibile.", + "collectionHelp": "Qualsiasi nome — OmniRoute lo crea al primo utilizzo", + "embeddingModelHelp": "Imposta automaticamente la dimensione del vettore al primo utilizzo. Le memorie esistenti non vengono popolate retroattivamente, e cambiare modello dopo aver inserito dati richiede una nuova collezione.", + "hostHelp": "Docker locale: http://localhost:6333 · Qdrant Cloud: l'URL del tuo cluster" }, "rerank": { "enableLabel": "__MISSING__:Enable Rerank", @@ -3538,7 +3565,8 @@ "installSkillModalDesc": "__MISSING__:Paste a skill manifest JSON or upload a .json file.", "uploadJson": "__MISSING__:Upload JSON", "cancel": "__MISSING__:Cancel", - "installSkill": "Installa abilità" + "installSkill": "Installa abilità", + "delete": "Elimina" }, "health": { "title": "Salute del sistema", @@ -4813,7 +4841,13 @@ "kimiWebLabel": "__MISSING__:Kimi Web", "kimiWebDesc": "__MISSING__:Moonshot AI consumer chat via www.kimi.com (international, Connect-RPC API)", "doubaoWebLabel": "__MISSING__:Doubao Web", - "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com" + "doubaoWebDesc": "__MISSING__:ByteDance AI chat via doubao.com", + "clientIdentityHint": "Opzionale. Aggiunge header di fingerprint client (es. User-Agent) corrispondenti a una CLI nota per gateway compatibili che li richiedono.", + "clientIdentityLabel": "Identità Client", + "compatibleDefaultModelHint": "Inserisci l'ID modello esattamente come lo aspetta il tuo endpoint compatibile. Questo modello verrà salvato come default della connessione.", + "compatibleDefaultModelLabel": "Modello Predefinito", + "iconUrlHint": "Opzionale. URL dell'immagine mostrata come icona di questo provider.", + "iconUrlLabel": "URL Icona" }, "settings": { "title": "Impostazioni", @@ -5955,6 +5989,12 @@ "INSUFFICIENT_SCOPE": "__MISSING__:API key lacks the manage scope.", "BYPASS_PREFIX_NOT_ALLOWED": "__MISSING__:One or more prefixes target spawn-capable routes and cannot be bypassed.", "GENERIC": "__MISSING__:Failed to update authz settings." + }, + "cors": { + "wildcard": { + "desc": "Qualsiasi sito web può chiamare l'API di questo server dal browser di un visitatore. Usa solo su reti fidate — imposta origini esplicite in ALLOWED_ORIGINS e disabilita CORS_ALLOW_ALL in produzione.", + "title": "CORS è aperto a tutte le origini (CORS_ALLOW_ALL=true)" + } } }, "resilienceBaseCooldownLabel": "Tempo di recupero della base", @@ -6059,7 +6099,56 @@ "modelLockoutExponentialBackoff": "__MISSING__:Exponential Backoff", "modelLockoutExponentialBackoffDescription": "__MISSING__:When enabled, each consecutive failure increases the cooldown duration exponentially.", "modelLockoutMaxBackoffSteps": "__MISSING__:Max Backoff Steps", - "modelLockoutMaxBackoffStepsDescription": "__MISSING__:Maximum number of backoff steps before the cooldown stops growing. The Max Cooldown cap is reached first in most configurations, making this a safety ceiling for when Max Cooldown is raised." + "modelLockoutMaxBackoffStepsDescription": "__MISSING__:Maximum number of backoff steps before the cooldown stops growing. The Max Cooldown cap is reached first in most configurations, making this a safety ceiling for when Max Cooldown is raised.", + "compressionCavemanPanelHint": "L'attivazione e il livello si impostano nel panel:", + "compressionPreserveSystemAlways": "Sempre", + "compressionPreserveSystemNever": "Mai", + "compressionPreserveSystemWhenNoCache": "Quando nessuna cache", + "description": "Descrizione", + "disable": "Disabilita", + "enable": "Abilita", + "logToolSourcesDescription": "Emette una riga di log diagnostico per richiesta che riepiloga il conteggio tool e la suddivisione per sorgente MCP/hosted/client.", + "logToolSourcesToggle": "Log Sorgenti Tool", + "logsDeleted": "{count, plural, =0 {Nessun log scaduto eliminato} one {Eliminato # log scaduto} other {Eliminati # log scaduti}}", + "queueDepth": "Profondità Coda", + "redisLauncherContainer": "Container", + "redisLauncherDesc": "Avvia con un clic un container Redis 7 (Podman o Docker) per cache risposte, tracciamento quote e rate limiting.", + "redisLauncherError": "Errore: {message}", + "redisLauncherHint": "Equivalente a eseguire `omniroute redis up`. Il container si chiama `omniroute-redis` e ascolta su 127.0.0.1:6379.", + "redisLauncherLaunch": "Avvia Redis", + "redisLauncherLaunching": "Avvio in corso...", + "redisLauncherReachable": "Raggiungibile", + "redisLauncherRefresh": "Aggiorna", + "redisLauncherRunning": "In esecuzione", + "redisLauncherStop": "Ferma", + "redisLauncherTitle": "Redis Locale", + "reset": "Ripristina", + "resetUsageData": "Ripristina Dati di Utilizzo", + "resetUsageDataDesc": "Seleziona fino a quando eliminare i dati di utilizzo. Questa azione non può essere annullata.", + "resetUsageFailed": "Ripristino dati di utilizzo fallito", + "resetUsagePeriod_12h": "12 ore", + "resetUsagePeriod_1d": "1 giorno", + "resetUsagePeriod_1h": "1 ora", + "resetUsagePeriod_30d": "30 giorni", + "resetUsagePeriod_3h": "3 ore", + "resetUsagePeriod_5m": "5 minuti", + "resetUsagePeriod_6h": "6 ore", + "resetUsagePeriod_7d": "7 giorni", + "resetUsagePeriod_all": "Tutto", + "resetUsageSuccess": "{count, plural, =0 {Nessuna riga dati utilizzo eliminata} one {Ripristinati dati utilizzo (# riga eliminata)} other {Ripristinati dati utilizzo (# righe eliminate)}}", + "resetting": "Ripristino in corso...", + "resilienceComboCooldownBudgetMs": "Budget attesa totale", + "resilienceComboCooldownMaxWaitMs": "Attesa massima per tentativo", + "resilienceComboCooldownWaitDesc": "Solo per combo quota-share: attende un breve cooldown transitorio e reinoltra invece di restituire subito un 429. Non attende mai su quota_exhausted.", + "resilienceComboCooldownWaitTitle": "Attesa cooldown combo quota-share", + "resilienceComboCooldownWaitToggleDesc": "Solo combo quota-share; non attende mai su quota_exhausted.", + "resilienceQuotaShareConcurrencyDesc": "Solo per combo quota-share: quando una connessione imposta un limite Max Concurrent, serializza le richieste concorrenti verso quell'account di sottoscrizione in modo che non venga mai inondato oltre il suo tetto. Le richieste in eccesso aspettano in coda invece di ricevere un 429. Il limite deriva dal campo Max Concurrent di ogni connessione; questo interruttore abilita o disabilita solo il suo rispetto.", + "resilienceQuotaShareConcurrencyTitle": "Concorrenza per connessione quota-share", + "resilienceQuotaShareConcurrencyToggleDesc": "Solo combo quota-share; rispetta il limite Max Concurrent di ogni connessione.", + "searchProviderAria": "Provider di ricerca", + "searchProviderPlaceholder": "Cerca provider...", + "selectProviderPlaceholder": "Seleziona provider...", + "update": "Aggiorna" }, "contextRtk": { "title": "RTK Engine", @@ -6837,7 +6926,9 @@ "updatedShort": "__MISSING__:Updated", "lastRefreshed": "__MISSING__:Last refreshed", "providerQuota": "__MISSING__:Provider Quota", - "providerQuotaHomeHint": "__MISSING__:Live status across connected accounts" + "providerQuotaHomeHint": "__MISSING__:Live status across connected accounts", + "showLessQuotas": "Mostra meno", + "showMoreQuotas": "Mostra {count} altri" }, "modals": { "waitingAuth": "In attesa di autorizzazione", @@ -7834,7 +7925,12 @@ "bulkImportMaxExceeded": "Massimo 100 proxy per importazione", "bulkImportPreview": "Anteprima", "clearAssignment": "(incarico chiaro)", - "bulkProxyAssignment": "Assegnazione di proxy in blocco" + "bulkProxyAssignment": "Assegnazione di proxy in blocco", + "batchDeleteSelected": "Elimina {count} selezionati", + "batchSelectedCount": "{count} selezionati", + "errorTestFailed": "Test dei proxy fallito", + "testAll": "Testa tutti", + "testPassed": "✓ OK" }, "playground": { "title": "Title", @@ -8376,7 +8472,8 @@ "verified": "__MISSING__:Verified", "install": "__MISSING__:Install", "installedFromMarketplace": "__MISSING__:Plugin {name} installed!", - "hooks": "Hooks" + "hooks": "Hooks", + "marketplaceInstallComingSoon": "Le installazioni dal marketplace saranno presto disponibili." }, "quotaPlans": { "title": "__MISSING__:Plans & Quotas", @@ -8606,7 +8703,8 @@ "goNow": "__MISSING__:Go now", "message": "__MISSING__:The MITM Proxy now lives under AgentBridge.", "title": "__MISSING__:This page has moved" - } + }, + "certManualTitle": "Il certificato non può essere installato automaticamente (es. dentro un container). Il bridge può comunque funzionare — aggiungi la CA manualmente:" }, "trafficInspector": { "title": "__MISSING__:Traffic Inspector", @@ -8932,5 +9030,31 @@ "colAvgScore": "__MISSING__:Avg Score", "colModels": "__MISSING__:Models", "colType": "__MISSING__:Type" + }, + "disabled": "Disabilitato", + "discovery": { + "title": "Provider Discovery", + "subtitle": "Scansiona i provider per metodi di accesso gratuiti/illimitati e rivedi i risultati. Opt-in, solo locale.", + "scanLabel": "Provider da scansionare", + "scanPlaceholder": "es. huggingchat", + "scan": "Scansiona", + "scanning": "Scansione in corso…", + "scanQueued": "Scansione accodata per {provider}.", + "scanFailed": "Scansione fallita.", + "loadFailed": "Caricamento risultati discovery fallito.", + "localOnlyNote": "Questo strumento è solo locale (loopback). Le scansioni vengono eseguite da questa macchina e non sono mai raggiungibili da remoto.", + "verify": "Verifica", + "verifyFailed": "Verifica del risultato fallita.", + "delete": "Elimina", + "deleteFailed": "Eliminazione del risultato fallita.", + "deleteTitle": "Elimina risultato discovery", + "deleteConfirm": "Eliminare il risultato discovery per {provider}? L'operazione non può essere annullata.", + "emptyTitle": "Nessun risultato discovery", + "emptyDescription": "Esegui una scansione qui sopra per cercare metodi di accesso gratuiti su un provider.", + "risk": "Rischio", + "method": "Metodo", + "auth": "Auth", + "feasibility": "Fattibilità", + "models": "Modelli" } -} +} \ No newline at end of file From 58abebd1065c0dd3a9b6b943ebb8151ce8aba151 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Jul 2026 07:34:49 -0300 Subject: [PATCH 009/109] test(dashboard): realign #6145 onboarding-href guard to the #6166 helper refactor (#6270) Realign the #6145 onboarding-href guard to the #6166 helper refactor (buildProviderDetailsHref). Test-only; unblocks the fast-path unit job across the open PR queue. Base-reds only (dast-smoke #6228, docs version-drift, executor-kiro anys). Integrated into release/v3.8.45. --- .../onboarding-wizard-details-link-6145.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/unit/onboarding-wizard-details-link-6145.test.ts b/tests/unit/onboarding-wizard-details-link-6145.test.ts index 78ff6c67a7..52efbd241b 100644 --- a/tests/unit/onboarding-wizard-details-link-6145.test.ts +++ b/tests/unit/onboarding-wizard-details-link-6145.test.ts @@ -9,6 +9,13 @@ import { dirname, join } from "node:path"; // `/dashboard/providers/[id]` route expects), NOT `connection.provider` (the // provider slug/type). The old code produced `/dashboard/providers/` // which 404s for openai-compatible / anthropic-compatible providers. +// +// #6166 refactored the inline `href={`/dashboard/providers/${connection.id}`}` +// literal into the tested `buildProviderDetailsHref(connection)` helper (its +// id-based routing + null-safety is guarded behaviorally in +// `provider-onboarding-href.test.ts`). This guard now tracks that refactor: the +// wizard must delegate to the helper and must NOT reintroduce a raw +// `connection.provider` URL. const here = dirname(fileURLToPath(import.meta.url)); const repoRoot = join(here, "..", ".."); @@ -20,11 +27,11 @@ const wizard = readFileSync( "utf8" ); -test("#6145: provider-details link routes by connection.id (matches the [id] route)", () => { +test("#6145: provider-details link routes through buildProviderDetailsHref (id-based helper)", () => { assert.match( wizard, - /href=\{`\/dashboard\/providers\/\$\{connection\.id\}`\}/, - "the details link must build the URL from connection.id" + /buildProviderDetailsHref\(connection\)/, + "the details link must be built by the tested buildProviderDetailsHref helper (routes by connection.id)" ); }); From 826a66f2878ac0b275365bbc0423107300334a46 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Jul 2026 07:37:17 -0300 Subject: [PATCH 010/109] feat(providers): add Yuanbao (web) cookie-session provider (#6196) (#6256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(providers): add Yuanbao (web) cookie-session provider (#6196). TDD-covered; base-reds only (dast-smoke #6228, docs version-drift, executor-kiro anys — #6145 guard fixed on tip via #6270). Integrated into release/v3.8.45. --- CHANGELOG.md | 4 + open-sse/config/providers/index.ts | 2 + .../providers/registry/yuanbao-web/index.ts | 37 ++ open-sse/executors/index.ts | 4 + open-sse/executors/yuanbao-web.ts | 504 ++++++++++++++++++ src/shared/constants/providers/web-cookie.ts | 15 + src/shared/providers/webSessionCredentials.ts | 7 + tests/snapshots/provider/translate-path.json | 23 + tests/unit/providers-yuanbao-web.test.ts | 201 +++++++ 9 files changed, 797 insertions(+) create mode 100644 open-sse/config/providers/registry/yuanbao-web/index.ts create mode 100644 open-sse/executors/yuanbao-web.ts create mode 100644 tests/unit/providers-yuanbao-web.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index eeefce72b1..442e2a572f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### ✨ New Features + +- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) + ### 🐛 Bug Fixes - **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`. diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index aa733053e6..f109a87171 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -134,6 +134,7 @@ import { agentrouterProvider } from "./registry/agentrouter/index.ts"; import { zaiProvider } from "./registry/zai/index.ts"; import { waferProvider } from "./registry/wafer/index.ts"; import { huggingchatProvider } from "./registry/huggingchat/index.ts"; +import { yuanbao_webProvider } from "./registry/yuanbao-web/index.ts"; import { galadrielProvider } from "./registry/galadriel/index.ts"; import { qianfanProvider } from "./registry/qianfan/index.ts"; import { meta_llamaProvider } from "./registry/meta-llama/index.ts"; @@ -314,6 +315,7 @@ export const REGISTRY: Record = { agentrouter: agentrouterProvider, zai: zaiProvider, huggingchat: huggingchatProvider, + "yuanbao-web": yuanbao_webProvider, galadriel: galadrielProvider, qianfan: qianfanProvider, "meta-llama": meta_llamaProvider, diff --git a/open-sse/config/providers/registry/yuanbao-web/index.ts b/open-sse/config/providers/registry/yuanbao-web/index.ts new file mode 100644 index 0000000000..c3dc4b1565 --- /dev/null +++ b/open-sse/config/providers/registry/yuanbao-web/index.ts @@ -0,0 +1,37 @@ +import type { RegistryEntry } from "../../shared.ts"; + +export const yuanbao_webProvider: RegistryEntry = { + id: "yuanbao-web", + alias: "ybw", + format: "openai", + executor: "yuanbao-web", + baseUrl: "https://yuanbao.tencent.com/api/chat", + authType: "apikey", + authHeader: "cookie", + models: [ + { id: "deepseek-v3", name: "DeepSeek V3 (via Yuanbao)", toolCalling: false }, + { + id: "deepseek-r1", + name: "DeepSeek R1 (via Yuanbao)", + supportsReasoning: true, + }, + { id: "hunyuan", name: "Hunyuan (via Yuanbao)" }, + { + id: "hunyuan-t1", + name: "Hunyuan T1 (via Yuanbao)", + supportsReasoning: true, + }, + { id: "deepseek-v3-search", name: "DeepSeek V3 + Web Search (via Yuanbao)" }, + { + id: "deepseek-r1-search", + name: "DeepSeek R1 + Web Search (via Yuanbao)", + supportsReasoning: true, + }, + { id: "hunyuan-search", name: "Hunyuan + Web Search (via Yuanbao)" }, + { + id: "hunyuan-t1-search", + name: "Hunyuan T1 + Web Search (via Yuanbao)", + supportsReasoning: true, + }, + ], +}; diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index 5984fde2ee..a3ab06cc6f 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -41,6 +41,7 @@ import { T3ChatWebExecutor } from "./t3-chat-web.ts"; import { ClaudeWebExecutor } from "./claude-web.ts"; import { InnerAiExecutor } from "./inner-ai.ts"; import { HuggingChatExecutor } from "./huggingchat.ts"; +import { YuanbaoWebExecutor } from "./yuanbao-web.ts"; import { PoeWebExecutor } from "./poe-web.ts"; import { VeniceWebExecutor } from "./venice-web.ts"; import { V0VercelWebExecutor } from "./v0-vercel-web.ts"; @@ -129,6 +130,8 @@ const executors = { "in-ai": new InnerAiExecutor(), // Alias huggingchat: new HuggingChatExecutor(), hc: new HuggingChatExecutor(), // Alias + "yuanbao-web": new YuanbaoWebExecutor(), + ybw: new YuanbaoWebExecutor(), // Alias "poe-web": new PoeWebExecutor(), poe: new PoeWebExecutor(), // Alias "venice-web": new VeniceWebExecutor(), @@ -212,6 +215,7 @@ export { ClaudeWebExecutor } from "./claude-web.ts"; export { DeepSeekWebExecutor } from "./deepseek-web.ts"; export { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-refresh.ts"; export { AdaptaWebExecutor } from "./adapta-web.ts"; +export { YuanbaoWebExecutor } from "./yuanbao-web.ts"; export { T3ChatWebExecutor } from "./t3-chat-web.ts"; export { InnerAiExecutor } from "./inner-ai.ts"; export { QwenWebExecutor } from "./qwen-web.ts"; diff --git a/open-sse/executors/yuanbao-web.ts b/open-sse/executors/yuanbao-web.ts new file mode 100644 index 0000000000..20ef5662e0 --- /dev/null +++ b/open-sse/executors/yuanbao-web.ts @@ -0,0 +1,504 @@ +/** + * YuanbaoWebExecutor — Tencent Yuanbao (yuanbao.tencent.com) Web Provider + * + * Routes chat requests through the Tencent Yuanbao consumer web session. + * Requires the `hy_user` + `hy_token` cookies from a logged-in + * yuanbao.tencent.com browser session (paste the full Cookie header). + * + * API flow (verified against the reverse-engineered references below): + * 1. POST /api/user/agent/conversation/create { agentId } -> { id } (conversationId) + * 2. POST /api/chat/{conversationId} (JSON body) -> SSE stream + * + * Streaming format (SSE, `data: {json}` lines): + * - { type: "think", content: "..." } -- reasoning tokens (DeepSeek-R1 / Hunyuan-T1) + * - { type: "text", msg: "..." } -- answer tokens + * - { ..., stopReason: "..." } -- terminal marker + * + * References (endpoint/payload/session shape lifted + cross-checked): + * - juzeon/yuanbao-chat2api (Rust) — cookie-only auth: hy_user + hy_token + agentId + * - chenwr727/yuanbao-free-api (Python) — endpoints, body shape, model map + */ +import { + BaseExecutor, + mergeAbortSignals, + mergeUpstreamExtraHeaders, + type ExecuteInput, +} from "./base.ts"; +import { FETCH_TIMEOUT_MS } from "../config/constants.ts"; +import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts"; +import { extractCookieValue, stripCookieInputPrefix } from "@/lib/providers/webCookieAuth"; + +const YUANBAO_BASE = "https://yuanbao.tencent.com"; +const CREATE_URL = `${YUANBAO_BASE}/api/user/agent/conversation/create`; +const CHAT_URL = `${YUANBAO_BASE}/api/chat`; + +// Public default DeepSeek agent id used by the Yuanbao web app. Not a secret — +// it is the shared consumer agent every logged-in session addresses by default. +const DEFAULT_AGENT_ID = "naQivTmsDa"; + +const USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"; + +const DEFAULT_MODEL = "deepseek-v3"; + +// OmniRoute model id -> Yuanbao internal chatModelId + optional supportFunctions. +const MODEL_MAP: Record = { + "deepseek-v3": { chatModelId: "deep_seek_v3" }, + "deepseek-r1": { chatModelId: "deep_seek" }, + "deepseek-v3-search": { + chatModelId: "deep_seek_v3", + supportFunctions: ["supportInternetSearch"], + }, + "deepseek-r1-search": { + chatModelId: "deep_seek", + supportFunctions: ["supportInternetSearch"], + }, + hunyuan: { chatModelId: "hunyuan_gpt_175B_0404" }, + "hunyuan-t1": { chatModelId: "hunyuan_t1" }, + "hunyuan-search": { + chatModelId: "hunyuan_gpt_175B_0404", + supportFunctions: ["supportInternetSearch"], + }, + "hunyuan-t1-search": { + chatModelId: "hunyuan_t1", + supportFunctions: ["supportInternetSearch"], + }, +}; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function isEncryptedCredentialBlob(value: unknown): boolean { + return typeof value === "string" && value.trim().startsWith("enc:v1:"); +} + +function extractText(content: unknown): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return String(content ?? ""); + return content + .map((part: unknown) => { + if (!part || typeof part !== "object") return ""; + const item = part as Record; + if ((item.type === "text" || item.type === "input_text") && typeof item.text === "string") { + return item.text; + } + return ""; + }) + .filter((p: string) => p.length > 0) + .join("\n"); +} + +/** Flatten OpenAI messages into the single-prompt shape Yuanbao expects. */ +function buildPrompt(messages: Array>): string { + const parts: Array<{ role: string; content: string }> = []; + for (const msg of messages) { + const role = String(msg.role || "user"); + const text = extractText(msg.content).trim(); + if (!text) continue; + parts.push({ role, content: text }); + } + if (parts.length === 0) return ""; + if (parts.length === 1) return parts[0].content; + // Multi-turn: label each turn (matches the reference chat2api formatting). + return parts.map((p) => `#[${p.role.trim()}]\n${p.content}`).join("\n\n"); +} + +/** Build the `hy_source=web; hy_user=...; hy_token=...` cookie from the pasted header. */ +function buildYuanbaoCookie(rawApiKey: string): { cookie: string; hasToken: boolean } { + const raw = stripCookieInputPrefix(rawApiKey || ""); + const hyUser = extractCookieValue(raw, "hy_user"); + const hyToken = extractCookieValue(raw, "hy_token"); + + if (hyUser && hyToken) { + return { cookie: `hy_source=web; hy_user=${hyUser}; hy_token=${hyToken}`, hasToken: true }; + } + + // Fall back to forwarding whatever the user pasted (may already be a full + // Cookie header). Only usable if it plausibly carries the session token. + const hasToken = raw.includes("hy_token="); + return { cookie: raw, hasToken }; +} + +function estimateTokens(text: string): number { + return Math.max(1, Math.ceil((text || "").length / 4)); +} + +async function readUpstreamErrorDetails(response: Response): Promise<{ + message: string | null; + details: unknown; +}> { + const contentType = response.headers.get("content-type") || ""; + const text = await response.text().catch(() => ""); + if (!text) return { message: null, details: null }; + + if (contentType.includes("json")) { + try { + const parsed = JSON.parse(text) as Record; + const message = + typeof parsed.message === "string" + ? parsed.message + : typeof parsed.error === "string" + ? parsed.error + : null; + return { message: message ? sanitizeErrorMessage(message) : null, details: parsed }; + } catch { + // fall through + } + } + return { message: sanitizeErrorMessage(text), details: { body: text } }; +} + +// ── Executor ──────────────────────────────────────────────────────────────── + +export class YuanbaoWebExecutor extends BaseExecutor { + constructor() { + super("yuanbao-web", { id: "yuanbao-web", baseUrl: CHAT_URL }); + } + + private errorResponse(status: number, message: string, url: string, details?: unknown) { + return { + response: new Response(JSON.stringify(buildErrorBody(status, message, details)), { + status, + headers: { "Content-Type": "application/json" }, + }), + url, + headers: {}, + transformedBody: undefined, + }; + } + + async execute(input: ExecuteInput): Promise<{ + response: Response; + url: string; + headers: Record; + transformedBody: unknown; + }> { + const { model, body, stream, credentials, signal, log, upstreamExtraHeaders } = input; + const messages = (body as Record).messages as + | Array> + | undefined; + + if (!messages || !Array.isArray(messages) || messages.length === 0) { + return this.errorResponse(400, "Missing or empty messages array", CHAT_URL); + } + + if (isEncryptedCredentialBlob(credentials.apiKey)) { + return this.errorResponse( + 401, + "Yuanbao credentials are encrypted but STORAGE_ENCRYPTION_KEY is not loaded. " + + "Restore the encryption key or re-save the Yuanbao cookie.", + CREATE_URL + ); + } + + const { cookie, hasToken } = buildYuanbaoCookie(credentials.apiKey || ""); + if (!hasToken) { + return this.errorResponse( + 401, + "Yuanbao requires a session cookie. Log in to yuanbao.tencent.com, open " + + "DevTools > Application > Cookies, and paste the full Cookie header " + + "(it must contain hy_user and hy_token).", + CREATE_URL + ); + } + + const resolvedModel = model && MODEL_MAP[model] ? model : DEFAULT_MODEL; + const modelSpec = MODEL_MAP[resolvedModel]; + const prompt = buildPrompt(messages); + if (!prompt.trim()) { + return this.errorResponse(400, "Empty prompt after processing messages", CHAT_URL); + } + + const baseHeaders: Record = { + Cookie: cookie, + "User-Agent": USER_AGENT, + Origin: YUANBAO_BASE, + Referer: `${YUANBAO_BASE}/chat/${DEFAULT_AGENT_ID}`, + "X-Agentid": DEFAULT_AGENT_ID, + }; + + const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS); + const combinedSignal = signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal; + + // ── Step 1: create conversation ───────────────────────────────────────── + let conversationId: string; + try { + const createRes = await fetch(CREATE_URL, { + method: "POST", + headers: { ...baseHeaders, "Content-Type": "application/json" }, + body: JSON.stringify({ agentId: DEFAULT_AGENT_ID }), + signal: combinedSignal, + }); + + if (!createRes.ok) { + const status = createRes.status; + const upstreamError = await readUpstreamErrorDetails(createRes); + let message = `Yuanbao conversation creation failed (HTTP ${status})`; + if (status === 401 || status === 403) { + message = + "Yuanbao auth failed — your hy_user/hy_token cookies may be missing or expired. " + + "Log in to yuanbao.tencent.com and re-paste your Cookie header."; + } else if (status === 429) { + message = "Yuanbao rate limited. Wait a moment and retry."; + } + if (upstreamError.message) message = `${message}: ${upstreamError.message}`; + return this.errorResponse(status, message, CREATE_URL, upstreamError.details); + } + + const createData = (await createRes.json()) as Record; + conversationId = String(createData.id || ""); + if (!conversationId) { + return this.errorResponse( + 502, + "Yuanbao did not return a conversation id", + CREATE_URL + ); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log?.error?.("YUANBAO-WEB", `Conversation creation failed: ${message}`); + return this.errorResponse( + 502, + `Yuanbao connection failed: ${sanitizeErrorMessage(message)}`, + CREATE_URL + ); + } + + // ── Step 2: send message ──────────────────────────────────────────────── + const messageUrl = `${CHAT_URL}/${conversationId}`; + const chatBody: Record = { + model: "gpt_175B_0404", + prompt, + plugin: "Adaptive", + displayPrompt: prompt, + displayPromptType: 1, + options: { + imageIntention: { + needIntentionModel: true, + backendUpdateFlag: 2, + intentionStatus: true, + }, + }, + multimedia: [], + agentId: DEFAULT_AGENT_ID, + supportHint: 1, + version: "v2", + chatModelId: modelSpec.chatModelId, + }; + if (modelSpec.supportFunctions) chatBody.supportFunctions = modelSpec.supportFunctions; + + const chatHeaders: Record = { + ...baseHeaders, + "Content-Type": "application/json", + Accept: "text/event-stream", + }; + mergeUpstreamExtraHeaders(chatHeaders, upstreamExtraHeaders); + + let upstreamResponse: Response; + try { + upstreamResponse = await fetch(messageUrl, { + method: "POST", + headers: chatHeaders, + body: JSON.stringify(chatBody), + signal: combinedSignal, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log?.error?.("YUANBAO-WEB", `Message send failed: ${message}`); + return this.errorResponse( + 502, + `Yuanbao connection failed: ${sanitizeErrorMessage(message)}`, + messageUrl + ); + } + + if (!upstreamResponse.ok) { + const status = upstreamResponse.status; + const upstreamError = await readUpstreamErrorDetails(upstreamResponse); + let message = `Yuanbao returned HTTP ${status}`; + if (status === 401 || status === 403) { + message = "Yuanbao auth failed — session cookie may be expired."; + } else if (status === 429) { + message = "Yuanbao rate limited. Wait a moment and retry."; + } + if (upstreamError.message) message = `${message}: ${upstreamError.message}`; + return this.errorResponse(status, message, messageUrl, upstreamError.details); + } + + if (!upstreamResponse.body) { + return this.errorResponse(502, "Yuanbao returned empty response body", messageUrl); + } + + // ── Step 3: translate SSE → OpenAI ────────────────────────────────────── + const id = `chatcmpl-yuanbao-${crypto.randomUUID().slice(0, 12)}`; + const created = Math.floor(Date.now() / 1000); + + if (stream) { + return { + response: new Response( + transformYuanbaoStream(upstreamResponse.body, resolvedModel, id, created, signal, log), + { + status: 200, + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + } + ), + url: messageUrl, + headers: chatHeaders, + transformedBody: chatBody, + }; + } + + const { content, reasoning } = await collectYuanbaoResponse(upstreamResponse.body, signal); + const completionTokens = estimateTokens(content + reasoning); + const messagePayload: Record = { role: "assistant", content }; + if (reasoning) messagePayload.reasoning_content = reasoning; + + return { + response: new Response( + JSON.stringify({ + id, + object: "chat.completion", + created, + model: resolvedModel, + choices: [{ index: 0, message: messagePayload, finish_reason: "stop" }], + usage: { + prompt_tokens: estimateTokens(prompt), + completion_tokens: completionTokens, + total_tokens: estimateTokens(prompt) + completionTokens, + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ), + url: messageUrl, + headers: chatHeaders, + transformedBody: chatBody, + }; + } +} + +// ── SSE translation helpers ─────────────────────────────────────────────────── + +interface YuanbaoEvent { + type?: string; + content?: string; + msg?: string; + stopReason?: string; +} + +function parseYuanbaoDataLine(line: string): YuanbaoEvent | null { + if (!line.startsWith("data: ")) return null; + const payload = line.slice(6).trim(); + if (!payload || payload === "[DONE]" || !payload.startsWith("{")) return null; + try { + return JSON.parse(payload) as YuanbaoEvent; + } catch { + return null; + } +} + +function transformYuanbaoStream( + upstream: ReadableStream, + model: string, + id: string, + created: number, + signal: AbortSignal | null | undefined, + log?: ExecuteInput["log"] +): ReadableStream { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + let roleEmitted = false; + + return new ReadableStream({ + async start(controller) { + const reader = upstream.getReader(); + let buffer = ""; + + const emit = (delta: object, finish?: string | null) => { + controller.enqueue( + encoder.encode( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created, + model, + choices: [{ index: 0, delta, finish_reason: finish ?? null }], + })}\n\n` + ) + ); + }; + + const ensureRole = () => { + if (!roleEmitted) { + roleEmitted = true; + emit({ role: "assistant", content: "" }); + } + }; + + try { + while (true) { + if (signal?.aborted) break; + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + const event = parseYuanbaoDataLine(line); + if (!event) continue; + if (event.type === "think" && event.content) { + ensureRole(); + emit({ reasoning_content: event.content }); + } else if (event.type === "text" && typeof event.msg === "string" && event.msg) { + ensureRole(); + emit({ content: event.msg }); + } + } + } + } catch (err) { + log?.error?.("YUANBAO-WEB", `Stream error: ${err}`); + } finally { + ensureRole(); + emit({}, "stop"); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + controller.close(); + reader.releaseLock(); + } + }, + }); +} + +async function collectYuanbaoResponse( + upstream: ReadableStream, + signal: AbortSignal | null | undefined +): Promise<{ content: string; reasoning: string }> { + const decoder = new TextDecoder(); + const reader = upstream.getReader(); + let buffer = ""; + let content = ""; + let reasoning = ""; + + try { + while (true) { + if (signal?.aborted) break; + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + const event = parseYuanbaoDataLine(line); + if (!event) continue; + if (event.type === "think" && event.content) reasoning += event.content; + else if (event.type === "text" && typeof event.msg === "string") content += event.msg; + } + } + } finally { + reader.releaseLock(); + } + + return { content, reasoning }; +} diff --git a/src/shared/constants/providers/web-cookie.ts b/src/shared/constants/providers/web-cookie.ts index c8000e6e30..39e0bbc053 100644 --- a/src/shared/constants/providers/web-cookie.ts +++ b/src/shared/constants/providers/web-cookie.ts @@ -185,6 +185,21 @@ export const WEB_COOKIE_PROVIDERS = { "Paste the full Cookie header from lmarena.ai (DevTools → Network → request → Cookie). The session is now split across arena-auth-prod-v1.0, .1, … — copy the whole header. Optional — works with free tier for basic comparisons.", riskNoticeVariant: "webCookie", }, + "yuanbao-web": { + id: "yuanbao-web", + alias: "ybw", + name: "Tencent Yuanbao (Free)", + icon: "auto_awesome", + color: "#0052D9", + textIcon: "YB", + website: "https://yuanbao.tencent.com", + hasFree: true, + freeNote: + "Free consumer web session — DeepSeek V3/R1 and Hunyuan / Hunyuan-T1, optional web search. No subscription required. Rate limits apply.", + authHint: + "Log in to yuanbao.tencent.com, then paste the full Cookie header (DevTools → Network → any /api request → Request Headers → Cookie). It must contain hy_user and hy_token.", + riskNoticeVariant: "webCookie", + }, huggingchat: { id: "huggingchat", // "hc" belongs to the hackclub provider; huggingchat uses its own id as alias. diff --git a/src/shared/providers/webSessionCredentials.ts b/src/shared/providers/webSessionCredentials.ts index 1a12284562..77d3d3ca5e 100644 --- a/src/shared/providers/webSessionCredentials.ts +++ b/src/shared/providers/webSessionCredentials.ts @@ -141,6 +141,13 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = { acceptsFullCookieHeader: true, storageKeys: ["cookie", "hf-chat"], }, + "yuanbao-web": { + kind: "cookie", + credentialName: "full Cookie header (hy_user + hy_token)", + placeholder: "hy_user=...; hy_token=... (full Cookie header from yuanbao.tencent.com)", + acceptsFullCookieHeader: true, + storageKeys: ["cookie", "hy_user", "hy_token"], + }, "poe-web": { kind: "cookie", credentialName: "p-b", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index f2f0f03029..428fe1abef 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -4402,6 +4402,29 @@ "stream": "https://api.lingyiwanwu.com/v1/chat/completions" } }, + "yuanbao-web": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://yuanbao.tencent.com/api/chat", + "stream": "https://yuanbao.tencent.com/api/chat" + } + }, "zai": { "format": "claude", "headers": { diff --git a/tests/unit/providers-yuanbao-web.test.ts b/tests/unit/providers-yuanbao-web.test.ts new file mode 100644 index 0000000000..2feab63371 --- /dev/null +++ b/tests/unit/providers-yuanbao-web.test.ts @@ -0,0 +1,201 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import type { ExecuteInput } from "../../open-sse/executors/base.ts"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const providers = await import("../../src/shared/constants/providers.ts"); +const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); +const { YuanbaoWebExecutor } = await import("../../open-sse/executors/yuanbao-web.ts"); + +type Dict = Record; +const registry = REGISTRY as unknown as Record; +const catalog = providers.WEB_COOKIE_PROVIDERS as unknown as Record; +const creds = (apiKey: string) => ({ apiKey }) as unknown as ExecuteInput["credentials"]; + +// ── Registry wiring ─────────────────────────────────────────────────────────── + +test("yuanbao-web is registered as a cookie-auth provider in the registry", () => { + const entry = registry["yuanbao-web"]; + assert.ok(entry, "yuanbao-web missing from REGISTRY"); + assert.equal(entry.id, "yuanbao-web"); + assert.equal(entry.alias, "ybw"); + assert.equal(entry.executor, "yuanbao-web"); + assert.equal(entry.format, "openai"); + assert.equal(entry.authHeader, "cookie"); + assert.equal(entry.baseUrl, "https://yuanbao.tencent.com/api/chat"); + const models = entry.models as Array<{ id: string }>; + assert.ok(Array.isArray(models) && models.length > 0); + const ids = models.map((m) => m.id); + assert.ok(ids.includes("deepseek-v3")); + assert.ok(ids.includes("hunyuan-t1")); +}); + +test("yuanbao-web appears in the web-cookie catalog with a cookie authHint", () => { + const entry = catalog["yuanbao-web"]; + assert.ok(entry, "yuanbao-web missing from WEB_COOKIE_PROVIDERS"); + assert.equal(entry.id, "yuanbao-web"); + assert.equal(entry.riskNoticeVariant, "webCookie"); + assert.match(String(entry.authHint), /hy_token/); + assert.match(String(entry.website), /yuanbao\.tencent\.com/); +}); + +test("YuanbaoWebExecutor is wired under id and alias", () => { + assert.ok(hasSpecializedExecutor("yuanbao-web")); + assert.ok(hasSpecializedExecutor("ybw")); + assert.ok(getExecutor("yuanbao-web") instanceof YuanbaoWebExecutor); + assert.ok(getExecutor("ybw") instanceof YuanbaoWebExecutor); +}); + +// ── Behavioral: SSE → OpenAI translation (mocked upstream) ───────────────────── + +function makeSSEBody(lines: string[]): ReadableStream { + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + for (const line of lines) controller.enqueue(encoder.encode(line)); + controller.close(); + }, + }); +} + +async function readStreamText(res: Response): Promise { + const reader = res.body!.getReader(); + const decoder = new TextDecoder(); + let out = ""; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + out += decoder.decode(value, { stream: true }); + } + return out; +} + +test("missing hy_token cookie returns a 401 auth error", async () => { + const exec = new YuanbaoWebExecutor(); + const { response } = await exec.execute({ + model: "deepseek-v3", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: creds("some_unrelated_cookie=abc"), + signal: null, + }); + assert.equal(response.status, 401); + const body = (await response.json()) as { error: { message: string } }; + assert.match(body.error.message, /hy_user|hy_token|session cookie/); + // Never leak stack traces. + assert.ok(!body.error.message.includes("at /")); +}); + +test("streaming request translates think/text events into OpenAI chunks", async () => { + const original = globalThis.fetch; + const calls: string[] = []; + globalThis.fetch = (async (url: string | URL | Request) => { + calls.push(String(url)); + if (String(url).includes("/conversation/create")) { + return new Response(JSON.stringify({ id: "conv-123" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response( + makeSSEBody([ + 'data: {"type":"think","content":"reasoning..."}\n', + 'data: {"type":"text","msg":"Hello"}\n', + 'data: {"type":"text","msg":" world"}\n', + 'data: {"stopReason":"stop"}\n', + ]), + { status: 200, headers: { "Content-Type": "text/event-stream" } } + ); + }) as typeof fetch; + + try { + const exec = new YuanbaoWebExecutor(); + const { response, url } = await exec.execute({ + model: "deepseek-r1", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: true, + credentials: creds("hy_source=web; hy_user=u1; hy_token=t1"), + signal: null, + }); + assert.equal(response.status, 200); + assert.match(url, /\/api\/chat\/conv-123$/); + assert.ok(calls[0].includes("/conversation/create")); + + const text = await readStreamText(response); + assert.match(text, /"reasoning_content":"reasoning\.\.\."/); + assert.match(text, /"content":"Hello"/); + assert.match(text, /"content":" world"/); + assert.match(text, /"finish_reason":"stop"/); + assert.match(text, /data: \[DONE\]/); + } finally { + globalThis.fetch = original; + } +}); + +test("non-streaming request collects content and reasoning", async () => { + const original = globalThis.fetch; + globalThis.fetch = (async (url: string | URL | Request) => { + if (String(url).includes("/conversation/create")) { + return new Response(JSON.stringify({ id: "conv-9" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response( + makeSSEBody([ + 'data: {"type":"think","content":"think-part"}\n', + 'data: {"type":"text","msg":"Answer"}\n', + 'data: {"stopReason":"stop"}\n', + ]), + { status: 200, headers: { "Content-Type": "text/event-stream" } } + ); + }) as typeof fetch; + + try { + const exec = new YuanbaoWebExecutor(); + const { response } = await exec.execute({ + model: "hunyuan-t1", + body: { messages: [{ role: "user", content: "q" }] }, + stream: false, + credentials: creds("hy_source=web; hy_user=u1; hy_token=t1"), + signal: null, + }); + assert.equal(response.status, 200); + const body = (await response.json()) as { + object: string; + model: string; + choices: Array<{ message: { content: string; reasoning_content?: string } }>; + }; + assert.equal(body.object, "chat.completion"); + assert.equal(body.choices[0].message.content, "Answer"); + assert.equal(body.choices[0].message.reasoning_content, "think-part"); + assert.equal(body.model, "hunyuan-t1"); + } finally { + globalThis.fetch = original; + } +}); + +test("upstream 401 on conversation create surfaces an auth error (no stack leak)", async () => { + const original = globalThis.fetch; + globalThis.fetch = (async () => + new Response(JSON.stringify({ message: "unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + })) as typeof fetch; + + try { + const exec = new YuanbaoWebExecutor(); + const { response } = await exec.execute({ + model: "deepseek-v3", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: creds("hy_source=web; hy_user=u1; hy_token=t1"), + signal: null, + }); + assert.equal(response.status, 401); + const body = (await response.json()) as { error: { message: string } }; + assert.ok(!body.error.message.includes("at /")); + } finally { + globalThis.fetch = original; + } +}); From 5531fc7f0589b87906c3fac6edc371d2c18adb44 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Jul 2026 07:41:15 -0300 Subject: [PATCH 011/109] feat(providers): route built-in agentrouter through dynamic CC wire image (#6056) (#6255) feat(providers): route built-in agentrouter through dynamic CC wire image (#6056). TDD-covered (agentrouter-cc-wire-image.test.ts). Base-reds only. Integrated into release/v3.8.45. --- CHANGELOG.md | 1 + .../providers/registry/agentrouter/index.ts | 16 +--- open-sse/services/ccWireImageBuiltins.ts | 26 ++++++ open-sse/services/claudeCodeCompatible.ts | 8 +- open-sse/services/provider.ts | 34 ++++++- tests/snapshots/provider/translate-path.json | 53 +++++------ tests/unit/agentrouter-cc-wire-image.test.ts | 91 +++++++++++++++++++ 7 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 open-sse/services/ccWireImageBuiltins.ts create mode 100644 tests/unit/agentrouter-cc-wire-image.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 442e2a572f..d67ed16ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### ✨ New Features - **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) +- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). ### 🐛 Bug Fixes diff --git a/open-sse/config/providers/registry/agentrouter/index.ts b/open-sse/config/providers/registry/agentrouter/index.ts index a4156d76ca..ebe9d4598f 100644 --- a/open-sse/config/providers/registry/agentrouter/index.ts +++ b/open-sse/config/providers/registry/agentrouter/index.ts @@ -1,14 +1,4 @@ import type { RegistryEntry } from "../../shared.ts"; -import { - getClaudeCliHeaders, - mapStainlessOs, - mapStainlessArch, - ANTHROPIC_BETA_CLAUDE_OAUTH, - ANTHROPIC_VERSION_HEADER, - CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, - CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, - CLAUDE_CLI_USER_AGENT, -} from "../../shared.ts"; export const agentrouterProvider: RegistryEntry = { id: "agentrouter", @@ -19,7 +9,11 @@ export const agentrouterProvider: RegistryEntry = { authType: "apikey", authHeader: "x-api-key", defaultContextLength: 128000, - headers: getClaudeCliHeaders(), + // No static `headers` here: agentrouter now adopts the DYNAMIC Claude-Code + // wire image via CC_WIRE_IMAGE_BUILTINS (#6056) — the fingerprint/headers are + // applied by buildProviderHeaders + applyFingerprint, keeping this entry's + // own baseUrl + x-api-key auth. A static fingerprint here would drift and + // trip AgentRouter's WAF ("unauthorized client detected"). models: [ { id: "claude-opus-4-6", name: "Claude 4.6 Opus" }, { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, diff --git a/open-sse/services/ccWireImageBuiltins.ts b/open-sse/services/ccWireImageBuiltins.ts new file mode 100644 index 0000000000..870e4f78f1 --- /dev/null +++ b/open-sse/services/ccWireImageBuiltins.ts @@ -0,0 +1,26 @@ +/** + * Built-in provider ids that must adopt the dynamic Claude-Code wire image + * (fingerprint headers/order + system transforms + `?beta=true` chat path) + * WITHOUT inheriting the Claude-Code-Compatible family's default anthropic + * baseUrl / Bearer auth. + * + * These providers keep their own registry `baseUrl` and auth scheme + * (e.g. `agentrouter` → `https://agentrouter.org/v1/messages` + `x-api-key`), + * while the two CC predicates (`isClaudeCodeCompatible` / + * `isClaudeCodeCompatibleProvider`) and `applyFingerprint` treat them as CC + * for the wire-image concerns only. The CC-baseUrl / CC-Bearer branches in + * `buildProviderUrl` / `buildProviderHeaders` are guarded so the registry + * baseUrl + auth are preserved. + * + * Single source of truth — imported by both predicates so they never diverge. + * See issue #6056. + */ +export const CC_WIRE_IMAGE_BUILTINS: ReadonlySet = new Set(["agentrouter"]); + +/** + * True when `provider` is a built-in that adopts the dynamic Claude-Code wire + * image while keeping its own registry baseUrl + auth. + */ +export function usesCcWireImage(provider: unknown): boolean { + return typeof provider === "string" && CC_WIRE_IMAGE_BUILTINS.has(provider); +} diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 4fd5f22711..eff67c4203 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -15,6 +15,7 @@ import { import { applyClaudeCodeCompatibleThinkingDisplay } from "./claudeCodeCompatibleThinkingDisplay.ts"; import { obfuscateInBody } from "./claudeCodeObfuscation.ts"; import { applySystemTransformPipeline, PROVIDER_CC_BRIDGE } from "./systemTransforms.ts"; +import { usesCcWireImage } from "./ccWireImageBuiltins.ts"; import { fixToolPairs, fixToolAdjacency, @@ -95,7 +96,12 @@ function supportsClaudeXHighEffort(model: string | null | undefined): boolean { } export function isClaudeCodeCompatibleProvider(provider: string | null | undefined): boolean { - return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX); + return ( + (typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) || + // Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image + // while keeping their own registry baseUrl + auth (#6056). + usesCcWireImage(provider) + ); } export function stripAnthropicMessagesSuffix(baseUrl: string | null | undefined): string { diff --git a/open-sse/services/provider.ts b/open-sse/services/provider.ts index 9149c7e70b..9ba0a0acff 100644 --- a/open-sse/services/provider.ts +++ b/open-sse/services/provider.ts @@ -8,6 +8,7 @@ import { } from "./claudeCodeCompatible.ts"; import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults"; import { buildClineHeaders } from "@/shared/utils/clineAuth"; +import { usesCcWireImage } from "./ccWireImageBuiltins.ts"; const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-"; const OPENAI_COMPATIBLE_DEFAULTS = { @@ -29,7 +30,12 @@ function isAnthropicCompatible(provider) { } export function isClaudeCodeCompatible(provider) { - return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX); + return ( + (typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) || + // Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image + // while keeping their own registry baseUrl + auth (#6056). + usesCcWireImage(provider) + ); } export function getOpenAICompatibleType( @@ -256,6 +262,15 @@ export function buildProviderUrl( providerSpecificData?: Record | null; } = {} ) { + // Built-in CC-wire-image providers (e.g. agentrouter): keep the registry's + // OWN baseUrl (NOT the CC family's anthropic default) but adopt the CC chat + // path so the request still targets `?beta=true` (#6056). + if (usesCcWireImage(provider)) { + const entry = getRegistryEntry(provider); + const config = getProviderConfig(provider); + const baseUrl = options?.baseUrl || entry?.baseUrl || config.baseUrl; + return joinClaudeCodeCompatibleUrl(baseUrl, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH); + } if (isOpenAICompatible(provider)) { const providerSpecificData = options?.providerSpecificData || null; const apiType = getOpenAICompatibleType(provider, providerSpecificData); @@ -318,12 +333,27 @@ export function buildProviderHeaders(provider, credentials, stream = true, body const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults( credentials?.providerSpecificData ); - return buildClaudeCodeCompatibleHeaders( + const ccHeaders = buildClaudeCodeCompatibleHeaders( token, stream, credentials?.providerSpecificData?.ccSessionId, { redactThinking: ccRequestDefaults.redactThinking === true } ); + // Built-in CC-wire-image providers (e.g. agentrouter): adopt the CC wire + // image headers but keep the registry's OWN auth scheme (e.g. x-api-key) + // instead of the CC family's Bearer auth (#6056). + if (usesCcWireImage(provider)) { + delete ccHeaders["Authorization"]; + const authHeader = entry?.authHeader || "bearer"; + if (authHeader === "x-api-key") { + if (token) ccHeaders["x-api-key"] = token; + } else if (authHeader === "key") { + if (token) ccHeaders["Authorization"] = `Key ${token}`; + } else { + ccHeaders["Authorization"] = `Bearer ${token}`; + } + } + return ccHeaders; } if (isAnthropicCompatible(provider)) { if (credentials.apiKey) { diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 428fe1abef..d760b0f7ac 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -27,64 +27,65 @@ "headers": { "apiKey": { "Accept": "text/event-stream", - "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07", - "Anthropic-Dangerous-Direct-Browser-Access": "true", - "Anthropic-Version": "2023-06-01", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.195 (external, cli)", - "X-App": "cli", + "User-Agent": "claude-cli/2.1.195 (external, sdk-cli)", "X-Stainless-Arch": "", - "X-Stainless-Helper-Method": "stream", "X-Stainless-Lang": "js", - "X-Stainless-Os": "", + "X-Stainless-OS": "MacOS", "X-Stainless-Package-Version": "0.94.0", "X-Stainless-Retry-Count": "0", "X-Stainless-Runtime": "node", "X-Stainless-Runtime-Version": "v24.3.0", "X-Stainless-Timeout": "600", - "x-api-key": "" + "accept-encoding": "gzip, deflate, br, zstd", + "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24", + "anthropic-dangerous-direct-browser-access": "true", + "anthropic-version": "2023-06-01", + "x-api-key": "", + "x-app": "cli" }, "nonStream": { - "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07", - "Anthropic-Dangerous-Direct-Browser-Access": "true", - "Anthropic-Version": "2023-06-01", + "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.195 (external, cli)", - "X-App": "cli", + "User-Agent": "claude-cli/2.1.195 (external, sdk-cli)", "X-Stainless-Arch": "", - "X-Stainless-Helper-Method": "stream", "X-Stainless-Lang": "js", - "X-Stainless-Os": "", + "X-Stainless-OS": "MacOS", "X-Stainless-Package-Version": "0.94.0", "X-Stainless-Retry-Count": "0", "X-Stainless-Runtime": "node", "X-Stainless-Runtime-Version": "v24.3.0", "X-Stainless-Timeout": "600", - "x-api-key": "" + "accept-encoding": "gzip, deflate, br, zstd", + "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24", + "anthropic-dangerous-direct-browser-access": "true", + "anthropic-version": "2023-06-01", + "x-api-key": "", + "x-app": "cli" }, "oauth": { "Accept": "text/event-stream", - "Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07", - "Anthropic-Dangerous-Direct-Browser-Access": "true", - "Anthropic-Version": "2023-06-01", "Content-Type": "application/json", - "User-Agent": "claude-cli/2.1.195 (external, cli)", - "X-App": "cli", + "User-Agent": "claude-cli/2.1.195 (external, sdk-cli)", "X-Stainless-Arch": "", - "X-Stainless-Helper-Method": "stream", "X-Stainless-Lang": "js", - "X-Stainless-Os": "", + "X-Stainless-OS": "MacOS", "X-Stainless-Package-Version": "0.94.0", "X-Stainless-Retry-Count": "0", "X-Stainless-Runtime": "node", "X-Stainless-Runtime-Version": "v24.3.0", "X-Stainless-Timeout": "600", - "x-api-key": "" + "accept-encoding": "gzip, deflate, br, zstd", + "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24", + "anthropic-dangerous-direct-browser-access": "true", + "anthropic-version": "2023-06-01", + "x-api-key": "", + "x-app": "cli" } }, "url": { - "nonStream": "https://agentrouter.org/v1/messages", - "stream": "https://agentrouter.org/v1/messages" + "nonStream": "https://agentrouter.org/v1/messages?beta=true", + "stream": "https://agentrouter.org/v1/messages?beta=true" } }, "agy": { diff --git a/tests/unit/agentrouter-cc-wire-image.test.ts b/tests/unit/agentrouter-cc-wire-image.test.ts new file mode 100644 index 0000000000..1cc334990d --- /dev/null +++ b/tests/unit/agentrouter-cc-wire-image.test.ts @@ -0,0 +1,91 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + isClaudeCodeCompatible, + buildProviderUrl, + buildProviderHeaders, +} from "../../open-sse/services/provider.ts"; +import { isClaudeCodeCompatibleProvider } from "../../open-sse/services/claudeCodeCompatible.ts"; +import { + CC_WIRE_IMAGE_BUILTINS, + usesCcWireImage, +} from "../../open-sse/services/ccWireImageBuiltins.ts"; +import { CLAUDE_CODE_COMPATIBLE_USER_AGENT } from "../../open-sse/services/claudeCodeCompatible.ts"; +import { CLAUDE_CLI_USER_AGENT } from "../../open-sse/config/anthropicHeaders.ts"; +import { applyFingerprint } from "../../open-sse/config/cliFingerprints.ts"; + +// Regression guard for #6056 — the built-in `agentrouter` provider must route +// through the DYNAMIC Claude-Code wire image (fingerprint headers + `?beta=true` +// chat path) while KEEPING its own registry baseUrl + x-api-key auth. + +test("agentrouter is registered in the CC-wire-image built-in allow-set", () => { + assert.ok(CC_WIRE_IMAGE_BUILTINS.has("agentrouter")); + assert.equal(usesCcWireImage("agentrouter"), true); + assert.equal(usesCcWireImage("claude"), false); + assert.equal(usesCcWireImage(null), false); +}); + +test("(a) both CC predicates return true for agentrouter", () => { + assert.equal(isClaudeCodeCompatible("agentrouter"), true); + assert.equal(isClaudeCodeCompatibleProvider("agentrouter"), true); +}); + +test("(a) predicates are unaffected for non-allow-set providers", () => { + // Official Claude OAuth provider must NOT be treated as CC-compatible. + assert.equal(isClaudeCodeCompatible("claude"), false); + assert.equal(isClaudeCodeCompatibleProvider("claude"), false); + // Genuine CC-family providers still match via the prefix. + assert.equal(isClaudeCodeCompatible("anthropic-compatible-cc-foo"), true); + assert.equal(isClaudeCodeCompatibleProvider("anthropic-compatible-cc-foo"), true); +}); + +test("(b) agentrouter outbound headers carry the dynamic CC wire image", () => { + const headers = buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true); + + // CC wire image markers (not the static getClaudeCliHeaders() shape). + assert.equal(headers["User-Agent"], CLAUDE_CODE_COMPATIBLE_USER_AGENT); + assert.notEqual(headers["User-Agent"], CLAUDE_CLI_USER_AGENT); + assert.equal(headers["x-app"], "cli"); + assert.equal(headers["anthropic-dangerous-direct-browser-access"], "true"); + assert.ok(headers["anthropic-beta"], "expected the CC anthropic-beta header"); + assert.ok(headers["X-Stainless-Package-Version"], "expected CC X-Stainless anchors"); +}); + +test("(b) applyFingerprint selects the claude-code-compatible fingerprint for agentrouter", () => { + const { headers } = applyFingerprint( + "agentrouter", + buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true), + { model: "claude-opus-4-6", messages: [] } + ); + // Fingerprint reordering keeps the CC wire image + the preserved x-api-key auth. + assert.equal(headers["x-api-key"], "sk-agentrouter"); + assert.equal(headers["User-Agent"], CLAUDE_CODE_COMPATIBLE_USER_AGENT); +}); + +test("(c) CRUX: agentrouter keeps its OWN x-api-key auth (NOT CC Bearer)", () => { + const headers = buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true); + assert.equal(headers["x-api-key"], "sk-agentrouter"); + assert.equal(headers["Authorization"], undefined); +}); + +test("(c) CRUX: agentrouter keeps its OWN registry baseUrl + ?beta=true", () => { + const url = buildProviderUrl("agentrouter", "claude-opus-4-6", true); + assert.equal(url, "https://agentrouter.org/v1/messages?beta=true"); + // NOT the CC-family anthropic default baseUrl. + assert.ok(!url.includes("api.anthropic.com")); +}); + +test("(c) real CC-family provider still uses the CC default baseUrl + Bearer auth", () => { + // The wire-image guard must NOT leak into genuine anthropic-compatible-cc-* providers. + const headers = buildProviderHeaders( + "anthropic-compatible-cc-foo", + { apiKey: "sk-foo" }, + true + ); + assert.equal(headers["Authorization"], "Bearer sk-foo"); + assert.equal(headers["x-api-key"], undefined); + + const url = buildProviderUrl("anthropic-compatible-cc-foo", "claude-sonnet-4-6", true); + assert.ok(url.includes("api.anthropic.com")); +}); From 776a7a3a587aebf4e6bd497c78ab0f5328cd6edf Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Jul 2026 07:42:29 -0300 Subject: [PATCH 012/109] feat(providers): bulk-add API keys for Cloudflare Workers AI (#6174) (#6254) feat(providers): bulk-add API keys for Cloudflare Workers AI (#6174). Per-entry providerSpecificData (fixes shared-object reuse); TDD guard bulk-api-key-parser-cloudflare.test.ts. Base-reds only. Integrated into release/v3.8.45. (thanks @muflifadla38) --- CHANGELOG.md | 1 + .../[id]/components/modals/AddApiKeyModal.tsx | 18 +- src/app/api/providers/bulk/route.ts | 13 +- src/i18n/messages/en.json | 3 +- src/shared/constants/providers.ts | 1 - src/shared/utils/bulkApiKeyParser.ts | 48 ++++- src/shared/validation/schemas/provider.ts | 15 ++ .../bulk-api-key-parser-cloudflare.test.ts | 176 ++++++++++++++++++ tests/unit/providers-bulk-route.test.ts | 5 +- 9 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 tests/unit/bulk-api-key-parser-cloudflare.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d67ed16ee6..21bc85eae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) - **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). +- **feat(providers):** **bulk-add API keys for Cloudflare Workers AI** ([#6174](https://github.com/diegosouzapw/OmniRoute/issues/6174)) — `cloudflare-ai` is removed from the bulk-add exclusion list and the bulk parser gains a 3-field `name|accountId|apiKey` mode; the bulk route now builds a **per-entry** `providerSpecificData` so each key carries its own `accountId` (fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard: `tests/unit/bulk-api-key-parser-cloudflare.test.ts`. (thanks @muflifadla38) ### 🐛 Bug Fixes diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index fd112a3b0c..ef0c741b89 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -348,7 +348,7 @@ export default function AddApiKeyModal({ const handleBulkSubmit = async () => { if (!provider) return; - const parsed = parseBulkApiKeys(bulkText); + const parsed = parseBulkApiKeys(bulkText, { withAccountId: isCloudflare }); setBulkWarnings(parsed.warnings); if (parsed.entries.length === 0) return; @@ -378,7 +378,11 @@ export default function AddApiKeyModal({ headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, - entries: parsed.entries.map((e) => ({ name: e.name, apiKey: e.apiKey })), + entries: parsed.entries.map((e) => ({ + name: e.name, + apiKey: e.apiKey, + ...(e.accountId ? { accountId: e.accountId } : {}), + })), priority: formData.priority || 1, providerSpecificData, validateKeys: bulkValidateKeys, @@ -457,12 +461,18 @@ export default function AddApiKeyModal({ {bulkSupported && mode === "bulk" && (
-

{t("bulkAddFormatHint")}

+

+ {isCloudflare ? t("bulkAddFormatHintCloudflare") : t("bulkAddFormatHint")} +

{openRouterPreset.input} {freeModelsToggle}
{t("colAvgScore")} {t("colModels")} {t("colType")}{t("colConfigured")}
{idx + 1} @@ -229,6 +278,17 @@ export default function FreeProviderRankingsPage() { {provider.category.toUpperCase()} + {configuredProviderIds.has(provider.id) ? ( + + ✓ + + ) : ( + + — + + )} +