From abe234e094bdbec72626505d31b1c5bb345cf334 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:25 +0200 Subject: [PATCH 01/20] fix(gamification): durable action-count badges that survive xp_audit_log pruning (#12651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Genuinely subtle: counting over `xp_audit_log` meant a 30-day prune silently redefined "lifetime" milestones as "last 30 days", so a key doing 900 req/month could never reach Token Consumer. A durable counter backfilled on migration, mirroring the `user_levels.total_xp` pattern, is the right shape. I renumbered the migration to 176 — 173 was taken by a migration that landed after you opened this — and synced the doc count, which the operator approved. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- AGENTS.md | 2 +- README.md | 2 +- .../12651-action-count-durable-counters.md | 1 + 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/el/llm.txt | 8 +- docs/i18n/es/llm.txt | 8 +- docs/i18n/et/llm.txt | 8 +- docs/i18n/fa/llm.txt | 8 +- docs/i18n/fi/llm.txt | 8 +- docs/i18n/fr/llm.txt | 8 +- docs/i18n/ga/llm.txt | 8 +- docs/i18n/gu/llm.txt | 8 +- docs/i18n/he/llm.txt | 8 +- docs/i18n/hi/llm.txt | 8 +- docs/i18n/hr/llm.txt | 8 +- docs/i18n/hu/llm.txt | 8 +- docs/i18n/id/llm.txt | 8 +- docs/i18n/it/llm.txt | 8 +- docs/i18n/ja/llm.txt | 8 +- docs/i18n/ko/llm.txt | 8 +- docs/i18n/lt/llm.txt | 8 +- docs/i18n/lv/llm.txt | 8 +- docs/i18n/mr/llm.txt | 8 +- docs/i18n/ms/llm.txt | 8 +- docs/i18n/mt/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/sl/llm.txt | 8 +- docs/i18n/sr/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 +- src/lib/db/gamification.ts | 15 +++ .../db/migrations/176_xp_action_counts.sql | 31 ++++++ src/lib/gamification/badges.ts | 19 ++-- src/lib/gamification/events.ts | 12 ++- .../action-count-durable-12546.test.ts | 96 +++++++++++++++++++ 59 files changed, 367 insertions(+), 219 deletions(-) create mode 100644 changelog.d/fixes/12651-action-count-durable-counters.md create mode 100644 src/lib/db/migrations/176_xp_action_counts.sql create mode 100644 tests/unit/gamification/action-count-durable-12546.test.ts diff --git a/AGENTS.md b/AGENTS.md index f98f367e52..c54ced4f59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below. | Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) | | Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions | | Services | `open-sse/services/` | Combo routing, rate limits, caching, etc | -| Database | `src/lib/db/` | SQLite domain modules (172 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (173 migrations) | | Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic | | MCP Server | `open-sse/mcp-server/` | 110 tools (45 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes | | A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | diff --git a/README.md b/README.md index 3c396da67a..9089b642b8 100644 --- a/README.md +++ b/README.md @@ -1253,7 +1253,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 172 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 173 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) diff --git a/changelog.d/fixes/12651-action-count-durable-counters.md b/changelog.d/fixes/12651-action-count-durable-counters.md new file mode 100644 index 0000000000..26e2f24728 --- /dev/null +++ b/changelog.d/fixes/12651-action-count-durable-counters.md @@ -0,0 +1 @@ +- **fix(gamification):** action-count badge milestones (First Token, Token Consumer, Token Machine, Token Whale, and the token-sharing tier) are now backed by a durable `xp_action_counts` counter incremented in `addXp()`, instead of a live `COUNT(*)` over `xp_audit_log`. The audit log is pruned by `retention.xpAuditLog` (default 30 days), so on a default install those "lifetime" milestones were really "actions in the last 30 days" and unlocked badges could stop unlocking once old rows aged out. `getActionCount()` and `checkActionCountBadges()` now read the same durable source, and a migration backfills existing totals from the surviving audit rows ([#12546](https://github.com/diegosouzapw/OmniRoute/issues/12546)) diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 5d46ea9cbf..2b04dd5611 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index bc5f6663bc..6885a44d7d 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index 2a6a8a6f28..c61ca0e2f2 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index fd2e50505a..483010c455 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index f0b7a8be0a..4ae9365d8b 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index 881686eef6..ee4520781c 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index 6c187e03f6..dc7fb5066c 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/el/llm.txt b/docs/i18n/el/llm.txt index edca8c385f..59b27596ec 100644 --- a/docs/i18n/el/llm.txt +++ b/docs/i18n/el/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index 6a7b34e3f8..6850fb637e 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/et/llm.txt b/docs/i18n/et/llm.txt index 1992d1c1a7..bdf82ff7e5 100644 --- a/docs/i18n/et/llm.txt +++ b/docs/i18n/et/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index df6a1d5b8b..60b88112a3 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 85d4bde1ec..5e3b031a59 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index 032c654417..26a9957669 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ga/llm.txt b/docs/i18n/ga/llm.txt index bf148cc7d4..4631bd5b16 100644 --- a/docs/i18n/ga/llm.txt +++ b/docs/i18n/ga/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index fa8a8d068f..21fde7a52f 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index 1121708c88..dcd6253495 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index b44f4375ae..2b99d0aadf 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hr/llm.txt b/docs/i18n/hr/llm.txt index 703e84d767..e18cca52bd 100644 --- a/docs/i18n/hr/llm.txt +++ b/docs/i18n/hr/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index 2480af9833..bbd8c65c2e 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index 2153feebf0..fbcafae8b5 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index 066da21716..6ee015b427 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index feac2fa0fd..29bcbbf27a 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 01f713eb85..f1913317ee 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/lt/llm.txt b/docs/i18n/lt/llm.txt index a9643a4b38..7a8a581c0b 100644 --- a/docs/i18n/lt/llm.txt +++ b/docs/i18n/lt/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/lv/llm.txt b/docs/i18n/lv/llm.txt index 2d1022ab4b..6eed1e4f65 100644 --- a/docs/i18n/lv/llm.txt +++ b/docs/i18n/lv/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 6df1e03304..a1cbcbf3b4 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index 1985fc6c9f..2255007a30 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/mt/llm.txt b/docs/i18n/mt/llm.txt index 119820a8d0..400d75b193 100644 --- a/docs/i18n/mt/llm.txt +++ b/docs/i18n/mt/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index 1197e3cde1..95aa98493f 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index e75c762333..07f3c2ed1d 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index 4b6eedf9de..4f136d96a4 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index 371c9e4488..9c2b4dba51 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index 2f0c4efbd8..e329102613 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index fae6ecbd40..8ac3192b48 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index 4729e6d8e7..f5a1f44c6d 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index 28e6800938..f8dec11b4b 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index 538e7b8385..3716887388 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sl/llm.txt b/docs/i18n/sl/llm.txt index 6a90516f38..045faba74a 100644 --- a/docs/i18n/sl/llm.txt +++ b/docs/i18n/sl/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sr/llm.txt b/docs/i18n/sr/llm.txt index de4a6f1ddf..a31af172f6 100644 --- a/docs/i18n/sr/llm.txt +++ b/docs/i18n/sr/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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 29925479a6..d4fa7537a1 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index 5827c5f7c0..e966703ced 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index 2b63c98999..49bdc10445 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index a6e3e1c4e2..7aec7e66e7 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index f436527b88..781710b7fd 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index 5a2353cede..f8d2b5c8d1 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index 16abdc65ae..1a85bab417 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index 94782940a2..8f127e9941 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index 1f112ba6b7..4d9a21d765 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 71e9415a9b..480200c746 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index 586a28205e..03401321dc 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -128,7 +128,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/llm.txt b/llm.txt index 6f73864505..8be5165779 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 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 173 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -124,7 +124,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo │ │ │ ├── secrets.ts # Secrets management │ │ │ ├── stateReset.ts # State reset utilities │ │ │ ├── migrationRunner.ts # Schema migration runner -│ │ │ └── migrations/ # 172 versioned SQL migration files +│ │ │ └── migrations/ # 173 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -389,7 +389,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages. -9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 173 SQL migrations. 10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`. @@ -433,7 +433,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 173 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/src/lib/db/gamification.ts b/src/lib/db/gamification.ts index 8bb5fba4aa..df452271a6 100644 --- a/src/lib/db/gamification.ts +++ b/src/lib/db/gamification.ts @@ -162,6 +162,21 @@ export function addXp(apiKeyId: string, action: string, amount: number, metadata ) .run(apiKeyId, action, amount, metadata ?? null); + // Durable per-key/per-action counter (#12546). xp_audit_log is pruned by + // retention.xpAuditLog (default 30 days), so counting action-count badge + // progress directly off that table silently reset every "lifetime" milestone. + // Increment a durable counter here, alongside the audit insert, using the same + // per-row weight getActionCount() reads: the metadata `amount` when present + // (token_share stores the shared amount there), otherwise 1. + db() + .prepare( + `INSERT INTO xp_action_counts (api_key_id, action, count, updated_at) + VALUES (?, ?, COALESCE(CAST(json_extract(?, '$.amount') AS INTEGER), 1), datetime('now')) + ON CONFLICT(api_key_id, action) + DO UPDATE SET count = count + excluded.count, updated_at = datetime('now')` + ) + .run(apiKeyId, action, metadata ?? null); + db() .prepare( `INSERT INTO user_levels (api_key_id, total_xp, current_level, updated_at) diff --git a/src/lib/db/migrations/176_xp_action_counts.sql b/src/lib/db/migrations/176_xp_action_counts.sql new file mode 100644 index 0000000000..5b2acd9b4e --- /dev/null +++ b/src/lib/db/migrations/176_xp_action_counts.sql @@ -0,0 +1,31 @@ +-- Migration 176: Durable per-key/per-action counters for gamification (#12546) +-- +-- getActionCount() (src/lib/gamification/badges.ts) and checkActionCountBadges() +-- (src/lib/gamification/events.ts) used to count rows directly in xp_audit_log, +-- which cleanupXpAuditLog() prunes by retention.xpAuditLog (default 30 days). So +-- the "lifetime" action-count milestones (First Token, Token Consumer, …) were +-- really "requests in the last 30 days" and were lost once the audit rows aged +-- out. This table keeps a durable running total per (api_key_id, action) that the +-- retention prune never touches — mirroring how user_levels.total_xp is a durable +-- aggregate rather than a live COUNT over xp_audit_log. + +CREATE TABLE IF NOT EXISTS xp_action_counts ( + api_key_id TEXT NOT NULL, + action TEXT NOT NULL, + count INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (api_key_id, action) +) WITHOUT ROWID; + +-- Backfill current lifetime totals from whatever xp_audit_log rows survive today. +-- Uses the same per-row weight getActionCount() applied: the metadata `amount` +-- when present (token_share records the shared amount there), otherwise 1. +-- INSERT OR IGNORE keeps the migration idempotent if it is ever re-executed. +INSERT OR IGNORE INTO xp_action_counts (api_key_id, action, count, updated_at) +SELECT + api_key_id, + action, + SUM(COALESCE(CAST(json_extract(metadata, '$.amount') AS INTEGER), 1)) AS count, + datetime('now') +FROM xp_audit_log +GROUP BY api_key_id, action; diff --git a/src/lib/gamification/badges.ts b/src/lib/gamification/badges.ts index 4111489d71..b7095c8202 100644 --- a/src/lib/gamification/badges.ts +++ b/src/lib/gamification/badges.ts @@ -319,7 +319,15 @@ type BadgeCriteria = // ─── Helper: Action Count ──────────────────────────────────────────────────── /** - * Get the total count of a specific action for an API key from the XP audit log. + * Get the durable lifetime count of a specific action for an API key. + * + * Reads the durable `xp_action_counts` counter (#12546) rather than counting + * rows in `xp_audit_log`. The audit log is pruned by `retention.xpAuditLog` + * (default 30 days), so counting it directly turned every "lifetime" + * action-count milestone into "actions in the last 30 days". The counter is + * incremented in `addXp()` alongside each audit insert and is never touched by + * the retention prune, so `checkActionCountBadges()` (events.ts) and this + * function now agree on the same durable source. */ async function getActionCount(apiKeyId: string, action: string): Promise { const { getDbInstance } = await import("../db/core"); @@ -327,14 +335,7 @@ async function getActionCount(apiKeyId: string, action: string): Promise const row = db .prepare( - `SELECT COALESCE(SUM( - CASE WHEN metadata IS NOT NULL - THEN CAST(json_extract(metadata, '$.amount') AS INTEGER) - ELSE 1 - END - ), 0) AS total - FROM xp_audit_log - WHERE api_key_id = ? AND action = ?` + `SELECT count AS total FROM xp_action_counts WHERE api_key_id = ? AND action = ?` ) .get(apiKeyId, action) as { total: number } | undefined; diff --git a/src/lib/gamification/events.ts b/src/lib/gamification/events.ts index 9bd52a8d24..8c2ad62369 100644 --- a/src/lib/gamification/events.ts +++ b/src/lib/gamification/events.ts @@ -172,14 +172,18 @@ async function checkActionCountBadges(apiKeyId: string, action: string): Promise const { getDbInstance } = await import("../db/core"); const db = getDbInstance(); - // Count total actions of this type + // Read the durable per-key/per-action counter (#12546), the same source + // getActionCount() (badges.ts) reads. Counting xp_audit_log directly here + // undercounted every "lifetime" milestone once the retention prune + // (cleanupXpAuditLog, default 30 days) aged the rows out. The counter is + // maintained in addXp() alongside the audit insert and survives the prune. const row = db .prepare( - "SELECT COALESCE(COUNT(*), 0) AS count FROM xp_audit_log WHERE api_key_id = ? AND action = ?" + "SELECT COALESCE(count, 0) AS count FROM xp_action_counts WHERE api_key_id = ? AND action = ?" ) - .get(apiKeyId, action) as { count: number }; + .get(apiKeyId, action) as { count: number } | undefined; - const count = row.count; + const count = row?.count ?? 0; // Badge thresholds const thresholds: Record> = { diff --git a/tests/unit/gamification/action-count-durable-12546.test.ts b/tests/unit/gamification/action-count-durable-12546.test.ts new file mode 100644 index 0000000000..61f302bae0 --- /dev/null +++ b/tests/unit/gamification/action-count-durable-12546.test.ts @@ -0,0 +1,96 @@ +/** + * #12546 — Action-count badges must survive xp_audit_log retention pruning. + * + * Regression guard for the durable per-key/per-action counter (Option A, + * endorsed by the maintainer). Before the fix, both getActionCount() + * (src/lib/gamification/badges.ts) and checkActionCountBadges() + * (src/lib/gamification/events.ts) counted rows directly in xp_audit_log, which + * cleanupXpAuditLog() prunes by retention.xpAuditLog (default 30 days). So on a + * default install a user who crossed a lifetime milestone lost the badge as soon + * as the audit rows aged out — the "lifetime" milestones were really + * "requests in the last 30 days". + * + * Each test drives real activity through addXp(), ages the audit rows past the + * retention window, runs the ACTUAL prune (cleanupXpAuditLog), and only then + * evaluates the badge. The durable counter must keep the badge unlockable. + * + * RED on base: the audit rows are gone, the count reads 0/1, the milestone + * badge never unlocks. GREEN with the fix: the durable counter still reads the + * lifetime total. + */ +import { describe, it, before } from "node:test"; +import assert from "node:assert/strict"; +import { addXp, hasBadge } from "../../../src/lib/db/gamification"; +import { evaluateBadges, seedBuiltinBadges } from "../../../src/lib/gamification/badges"; +import { emitGamificationEvent } from "../../../src/lib/gamification/events"; +import { cleanupXpAuditLog } from "../../../src/lib/db/cleanup"; +import { getDbInstance } from "../../../src/lib/db/core"; + +// token-consumer requires 1,000 lifetime "request" actions. Using a milestone +// well above 1 keeps the discriminant robust: a single fresh event emitted after +// the prune can never satisfy it from the (empty) audit log alone. +const CONSUMER_THRESHOLD = 1000; + +function seedLifetimeRequests(apiKeyId: string, n: number): void { + for (let i = 0; i < n; i++) { + addXp(apiKeyId, "request", 1); + } +} + +function ageAndPruneAuditLog(apiKeyId: string): void { + const db = getDbInstance(); + // Push the audit rows well past the default 30-day retention window. + db.prepare("UPDATE xp_audit_log SET created_at = datetime('now', '-60 days') WHERE api_key_id = ?").run( + apiKeyId + ); +} + +describe("#12546 action-count badges survive xp_audit_log pruning", () => { + before(async () => { + await seedBuiltinBadges(); + }); + + it("evaluateBadges() still unlocks the lifetime milestone after the audit log is pruned", async () => { + const key = `dc-eval-${Date.now()}`; + const db = getDbInstance(); + + seedLifetimeRequests(key, CONSUMER_THRESHOLD); + ageAndPruneAuditLog(key); + + const pruneResult = await cleanupXpAuditLog(); + assert.ok(pruneResult.deleted >= CONSUMER_THRESHOLD, "the prune must have deleted the aged rows"); + + const remaining = db + .prepare("SELECT COUNT(*) AS c FROM xp_audit_log WHERE api_key_id = ?") + .get(key) as { c: number }; + assert.equal(remaining.c, 0, "sanity: no audit rows remain for this key after the prune"); + + // getActionCount() (the function named in the issue) is exercised through + // evaluateBadges(). With the durable counter it still reads the lifetime + // total; against the pruned audit log it reads 0. + const unlocked = await evaluateBadges(key, "request"); + assert.ok( + unlocked.includes("token-consumer"), + "token-consumer must unlock from the durable counter after the audit log is pruned" + ); + }); + + it("checkActionCountBadges() (via emitGamificationEvent) still unlocks the milestone after pruning", async () => { + const key = `dc-emit-${Date.now()}`; + + seedLifetimeRequests(key, CONSUMER_THRESHOLD); + ageAndPruneAuditLog(key); + await cleanupXpAuditLog(); + + // A single fresh request. On base this leaves exactly one audit row, so the + // COUNT(*) source reads 1 (< 1000) and the badge stays locked. With the fix, + // checkActionCountBadges() reads the durable counter (>= 1000) and unlocks. + await emitGamificationEvent({ apiKeyId: key, action: "request" }); + + assert.equal( + hasBadge(key, "token-consumer"), + true, + "token-consumer must unlock via the events.ts path from the durable counter" + ); + }); +}); From 4309a2fd56540ee3d0f7b8a2255a1c63231bdc13 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:29 +0200 Subject: [PATCH 02/20] fix(antigravity): preserve thought token usage (#13055) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right fix: `thoughtsTokenCount` is real output the caller paid for, so folding it into `completion_tokens` and surfacing it as `completion_tokens_details.reasoning_tokens` matches what every other reasoning-capable provider reports. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../13055-antigravity-thought-token-usage.md | 1 + open-sse/executors/antigravity/sseCollect.ts | 9 ++++-- tests/unit/executor-antigravity.test.ts | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/13055-antigravity-thought-token-usage.md diff --git a/changelog.d/fixes/13055-antigravity-thought-token-usage.md b/changelog.d/fixes/13055-antigravity-thought-token-usage.md new file mode 100644 index 0000000000..f19f31a0a5 --- /dev/null +++ b/changelog.d/fixes/13055-antigravity-thought-token-usage.md @@ -0,0 +1 @@ +- **fix(antigravity):** Preserve upstream thought-token usage in normalized completion and reasoning token counts ([#13055](https://github.com/diegosouzapw/OmniRoute/pull/13055)) — thanks @pacocartones diff --git a/open-sse/executors/antigravity/sseCollect.ts b/open-sse/executors/antigravity/sseCollect.ts index 5b7ef3ea85..ee4de7c11b 100644 --- a/open-sse/executors/antigravity/sseCollect.ts +++ b/open-sse/executors/antigravity/sseCollect.ts @@ -18,8 +18,7 @@ export type AntigravityCollectedStream = { // Both run once per SSE data line / per text part (processAntigravitySSEPayload), // so the literals are hoisted to module constants. -const TEXTUAL_TOOL_CALL_RE = - /^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/; +const TEXTUAL_TOOL_CALL_RE = /^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/; export function stripZeroWidth(value: unknown): unknown { if (typeof value === "string") { @@ -145,10 +144,14 @@ export function processAntigravitySSEPayload( } if (parsed?.response?.usageMetadata) { const um = parsed.response.usageMetadata; + const thoughtsTokens = typeof um.thoughtsTokenCount === "number" ? um.thoughtsTokenCount : 0; collected.usage = { prompt_tokens: um.promptTokenCount || 0, - completion_tokens: um.candidatesTokenCount || 0, + completion_tokens: (um.candidatesTokenCount || 0) + thoughtsTokens, total_tokens: um.totalTokenCount || 0, + ...(thoughtsTokens > 0 + ? { completion_tokens_details: { reasoning_tokens: thoughtsTokens } } + : {}), }; } if (Array.isArray(parsed?.remainingCredits)) { diff --git a/tests/unit/executor-antigravity.test.ts b/tests/unit/executor-antigravity.test.ts index facd6ba68b..befff78723 100644 --- a/tests/unit/executor-antigravity.test.ts +++ b/tests/unit/executor-antigravity.test.ts @@ -36,6 +36,7 @@ type ChatCompletionPayload = { prompt_tokens: number; completion_tokens: number; total_tokens: number; + completion_tokens_details?: { reasoning_tokens: number }; }; }; @@ -482,6 +483,33 @@ test("AntigravityExecutor.collectStreamToResponse turns SSE Gemini chunks into a }); }); +test("AntigravityExecutor.collectStreamToResponse preserves upstream thought token usage", async () => { + const executor = new AntigravityExecutor(); + const response = new Response( + 'data: {"response":{"candidates":[{"content":{"parts":[{"text":"Done"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":5,"candidatesTokenCount":3,"thoughtsTokenCount":7,"totalTokenCount":15}}}\n\n', + { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + } + ); + + const result = await executor.collectStreamToResponse( + response, + "gemini-3.7-pro-high", + "https://example.com", + { Authorization: "Bearer ag-token" }, + { request: {} } + ); + const payload = (await result.response.json()) as ChatCompletionPayload; + + assert.deepEqual(payload.usage, { + prompt_tokens: 5, + completion_tokens: 10, + total_tokens: 15, + completion_tokens_details: { reasoning_tokens: 7 }, + }); +}); + test("AntigravityExecutor.collectStreamToResponse converts textual tool call SSE to structured tool_calls", async () => { const executor = new AntigravityExecutor(); const response = new Response( From a0c52ba54dc899967c2cbb4cf1570c664eccc109 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:33 +0200 Subject: [PATCH 03/20] fix(images): fall through combo edit targets on /v1/images/edits (#12653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The asymmetry was the bug: `/generations` iterated every image-capable target while `/edits` only ever tried the first. Extracting the iteration into `runImageComboTargets` and proving `/generations` byte-identical before wiring `/edits` onto it is the right order. Missing credentials skipping rather than hard-401 now matches generations too. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../fixes/12653-image-combo-edits-fallback.md | 1 + open-sse/services/imageCombo.ts | 241 ++++++++----- src/app/api/v1/images/edits/route.ts | 321 ++++++++++++++++++ .../image-combo-edits-fallback-12547.test.ts | 219 ++++++++++++ 4 files changed, 705 insertions(+), 77 deletions(-) create mode 100644 changelog.d/fixes/12653-image-combo-edits-fallback.md create mode 100644 tests/unit/image-combo-edits-fallback-12547.test.ts diff --git a/changelog.d/fixes/12653-image-combo-edits-fallback.md b/changelog.d/fixes/12653-image-combo-edits-fallback.md new file mode 100644 index 0000000000..4d143b37c1 --- /dev/null +++ b/changelog.d/fixes/12653-image-combo-edits-fallback.md @@ -0,0 +1 @@ +- **fix(images):** `/v1/images/edits` now iterates a combo's targets the same way `/v1/images/generations` does (#9239) instead of flattening a bare combo to its first target. A combo whose first target is not edit-capable — or lacks credentials — now falls through to a later edit-capable target rather than hard-erroring, and missing credentials are skipped (not a hard `401`) to match the generations path. The per-target skip/terminal classification is extracted into a shared `runImageComboTargets` loop, so generations behavior is unchanged ([#12547](https://github.com/diegosouzapw/OmniRoute/issues/12547)). diff --git a/open-sse/services/imageCombo.ts b/open-sse/services/imageCombo.ts index 650829d2b2..3b8fd0998f 100644 --- a/open-sse/services/imageCombo.ts +++ b/open-sse/services/imageCombo.ts @@ -34,6 +34,141 @@ type ImageGenerationResult = | { success: true; data?: unknown; status?: number; error?: string } | { success: false; data?: unknown; status?: number; error?: string }; +/** Minimum shape a combo target must expose to be iterated. */ +export interface ImageComboTarget { + modelStr: string; +} + +/** Normalized per-target dispatch result (success or classified failure). */ +export interface ImageComboDispatchResult { + success: boolean; + data?: unknown; + status?: number; + error?: unknown; +} + +/** + * Outcome of iterating a combo's targets. + * - `success`: a target produced an image; `data` is the handler payload. + * - `terminal`: a target failed with a terminal status (400/401/403); the caller + * should surface it as a hard error and stop. + * - `exhausted`: every target was skipped or failed non-terminally. + */ +export type RunImageComboTargetsResult = + | { outcome: "success"; provider: string; model: string; data: unknown; fallbackCount: number } + | { outcome: "terminal"; provider: string; status: number; error: string; fallbackCount: number } + | { + outcome: "exhausted"; + fallbackCount: number; + lastError: { status: number; error: string } | null; + }; + +export interface RunImageComboTargetsOptions { + /** Map a target to its `{ provider, model }`. An empty provider skips the target. */ + resolveProvider: (target: T) => { provider: string | null; model: string | null }; + /** Resolve credentials for a target. Throwing is treated as a transient skip. */ + resolveCredentials: (provider: string, target: T) => Promise; + /** Rate-limit predicate; defaults to isAllRateLimitedCredentials. */ + isRateLimited?: (credentials: unknown) => boolean; + /** Perform the actual per-target work (generation or edit) with resolved credentials. */ + dispatch: (ctx: { + target: T; + provider: string; + model: string; + credentials: unknown; + }) => Promise; + /** Invoked once on the winning target's credentials (e.g. clear recovered state). */ + onSuccess?: (credentials: unknown) => Promise; + /** Default error text when a dispatch failure carries no string error. */ + failureLabel?: string; +} + +/** + * Iterate combo targets in priority order, applying the shared skip / terminal + * classification that both /v1/images/generations and /v1/images/edits rely on: + * + * - missing credentials, DB errors, and rate-limited accounts are skipped + * (fall through to the next target) rather than terminating the request; + * - a 400/401/403 from an actual dispatch attempt is terminal (stop iterating); + * - any other dispatch failure (429/5xx) is non-terminal (try the next target); + * - the first success wins. + * + * The only generation-vs-edit differences are injected via `resolveProvider`, + * `resolveCredentials`, and `dispatch`, so both routes share one loop (#12547). + */ +export async function runImageComboTargets( + targets: T[], + opts: RunImageComboTargetsOptions +): Promise { + const isRateLimited = opts.isRateLimited ?? isAllRateLimitedCredentials; + const failureLabel = opts.failureLabel ?? "Image generation failed"; + let lastError: { status: number; error: string } | null = null; + let fallbackCount = 0; + + for (const target of targets) { + const { provider, model } = opts.resolveProvider(target); + if (!provider) { + lastError = { status: 400, error: `Invalid image model: ${target.modelStr}` }; + fallbackCount += 1; + continue; + } + + // Resolve provider credentials + let credentials: unknown = null; + try { + credentials = await opts.resolveCredentials(provider, target); + } catch { + // DB unavailable — skip this target + lastError = { status: 502, error: `Failed to resolve credentials for ${provider}` }; + fallbackCount += 1; + continue; + } + + if (!credentials) { + lastError = { status: 400, error: `No credentials for image provider: ${provider}` }; + fallbackCount += 1; + continue; + } + + if (isRateLimited(credentials)) { + lastError = { + status: 429, + error: `[${provider}] All accounts rate limited`, + }; + fallbackCount += 1; + continue; + } + + const result = await opts.dispatch({ target, provider, model: model ?? "", credentials }); + + if (result.success) { + if (opts.onSuccess) await opts.onSuccess(credentials); + return { + outcome: "success", + provider, + model: model ?? "", + data: result.data, + fallbackCount, + }; + } + + // Classify the failure + const status = result.status || 500; + const error = typeof result.error === "string" ? result.error : failureLabel; + + // Terminal failures (400 bad model, 403 banned, etc.) — stop iterating + // Non-terminal failures (429, 5xx) — try next target + if (status === 400 || status === 403 || status === 401) { + return { outcome: "terminal", provider, status, error, fallbackCount }; + } + + lastError = { status, error: `[${provider}] ${error}` }; + fallbackCount += 1; + } + + return { outcome: "exhausted", fallbackCount, lastError }; +} + /** * Execute a full combo strategy for an image generation request. * @@ -80,86 +215,38 @@ export async function executeImageCombo( ); } - // 3. Iterate targets in priority order (first healthy target wins) - let lastError: { status: number; error: string } | null = null; - let successResult: { data: unknown; provider: string; model: string } | null = null; - let fallbackCount = 0; - let selectedProvider = ""; - let selectedModel = ""; + // 3. Iterate targets in priority order (first healthy target wins). + // The skip / terminal classification lives in the shared runImageComboTargets + // loop; generation only injects its own dispatch (handleImageGeneration) so + // /v1/images/edits can reuse the exact same iteration semantics (#12547). + const run = await runImageComboTargets(imageTargets, { + resolveProvider: (target) => parseImageModel(target.modelStr), + resolveCredentials: (provider) => getProviderCredentialsWithQuotaPreflight(provider), + dispatch: async ({ target, credentials }) => + (await handleImageGeneration({ + body: { ...body, model: target.modelStr }, + credentials, + log, + signal: auth.request?.signal || null, + })) as ImageGenerationResult, + onSuccess: async (credentials) => { + await clearRecoveredProviderState(credentials as never); + }, + failureLabel: "Image generation failed", + }); - for (const target of imageTargets) { - const { provider: targetProvider, model: targetModel } = parseImageModel(target.modelStr); - if (!targetProvider) { - lastError = { status: 400, error: `Invalid image model: ${target.modelStr}` }; - fallbackCount += 1; - continue; - } - - // Resolve provider credentials - let credentials = null; - try { - credentials = await getProviderCredentialsWithQuotaPreflight(targetProvider); - } catch { - // DB unavailable — skip this target - lastError = { status: 502, error: `Failed to resolve credentials for ${targetProvider}` }; - fallbackCount += 1; - continue; - } - - if (!credentials) { - lastError = { status: 400, error: `No credentials for image provider: ${targetProvider}` }; - fallbackCount += 1; - continue; - } - - if (isAllRateLimitedCredentials(credentials)) { - lastError = { - status: 429, - error: `[${targetProvider}] All accounts rate limited`, - }; - fallbackCount += 1; - continue; - } - - // Execute image generation for this target - const result = (await handleImageGeneration({ - body: { ...body, model: target.modelStr }, - credentials, - log, - signal: auth.request?.signal || null, - })) as ImageGenerationResult; - - if (result.success) { - await clearRecoveredProviderState(credentials); - selectedProvider = targetProvider; - selectedModel = target.modelStr; - successResult = { - data: result.data, - provider: targetProvider, - model: target.modelStr, - }; - break; - } - - // Classify the failure - const status = result.status || 500; - const error = typeof result.error === "string" ? result.error : "Image generation failed"; - - // Terminal failures (400 bad model, 403 banned, etc.) — stop iterating - // Non-terminal failures (429, 5xx) — try next target - if (status === 400 || status === 403 || status === 401) { - return errorResponse(status, `[${targetProvider}] ${error}`); - } - - lastError = { status, error: `[${targetProvider}] ${error}` }; - fallbackCount += 1; + // Terminal failure (400 bad model, 401/403 banned, etc.) — surface as a hard error. + if (run.outcome === "terminal") { + return errorResponse(run.status, `[${run.provider}] ${run.error}`); } // 4. Build response - if (successResult) { + if (run.outcome === "success") { + const selectedProvider = run.provider; + const selectedModel = run.model; // handleImageGeneration() already returns the public OpenAI images payload // ({ created, data: [...] }); count the images at that level (#12268). - const payload = successResult.data as { created?: number; data?: unknown[] } | unknown[]; + const payload = run.data as { created?: number; data?: unknown[] } | unknown[]; const images = Array.isArray(payload) ? payload : payload?.data; const n = Math.max(Number(body.n) || 1, images?.length || 0); const costUsd = await calculateModalCost("image", selectedProvider, selectedModel, { n }); @@ -172,7 +259,7 @@ export async function executeImageCombo( latencyMs: Date.now() - startTime, requestId: generateRequestId(), strategy: "priority", - fallbackAttempts: fallbackCount, + fallbackAttempts: run.fallbackCount, }); // Return the handler payload unchanged so the combo path matches the @@ -186,11 +273,11 @@ export async function executeImageCombo( // All targets failed — return the last error const errorPayload = toJsonErrorPayload( - lastError?.error || "All combo targets failed", + run.lastError?.error || "All combo targets failed", "Image combo targets all failed" ); return new Response(JSON.stringify(errorPayload), { - status: lastError?.status || 502, + status: run.lastError?.status || 502, headers: { "Content-Type": "application/json" }, }); } diff --git a/src/app/api/v1/images/edits/route.ts b/src/app/api/v1/images/edits/route.ts index 63da01ed1b..31da197f94 100644 --- a/src/app/api/v1/images/edits/route.ts +++ b/src/app/api/v1/images/edits/route.ts @@ -21,11 +21,19 @@ import { } from "@omniroute/open-sse/config/imageRegistry.ts"; import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; +import { getComboByName, getCombos } from "@/lib/db/combos"; +import { resolveComboTargets } from "@omniroute/open-sse/services/combo.ts"; +import { + runImageComboTargets, + type ImageComboDispatchResult, +} from "@omniroute/open-sse/services/imageCombo.ts"; +import { isAllRateLimitedCredentials } from "@/app/api/v1/_shared/rateLimit"; import * as log from "@/sse/utils/logger"; import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; import { resolveImageRouteModel, + resolveImageModelPrefix, extractImageEditInputFromJson, validateCodexImageEditReferences, } from "@/lib/images/imageRouteModel"; @@ -294,6 +302,286 @@ async function handleAdobeFireflyEditRequest(params: { ); } +/** Reference/prompt payload an edit dispatch needs, shared by single + combo paths. */ +interface ImageEditContext { + prompt: string; + size: string | null; + responseFormat: string | null; + images: Array<{ bytes: Buffer; mime: string }>; + imageBytes: Buffer | null; + imageMime: string | null; + imageInputCount: number; + allowedConnections: string[] | null; + request: Request; +} + +/** A combo target that resolved to an edit-capable provider/node. */ +interface EditComboTarget { + modelStr: string; + parsed: ReturnType; + providerConfig: ReturnType | null; + /** Credential/connection lookup key (built-in provider id, or custom node id). */ + credKey: string; +} + +/** + * Decide whether a prefix-resolved combo target can service an image edit, and + * return the credential key to resolve it with. Mirrors postHandler's provider + * branches: codex-responses, fal-ai edit models, adobe-firefly, built-in + * openrouter, and custom OpenAI-compatible nodes are edit-capable; every other + * built-in provider is not (it exposes no OpenAI-compatible edit endpoint). + */ +function classifyImageEditTarget( + resolvedModel: string, + parsed: ReturnType, + providerConfig: ReturnType | null +): { credKey: string } | null { + if (providerConfig) { + if ( + providerConfig.format === "codex-responses" || + providerConfig.format === "adobe-firefly-image" || + (providerConfig.format === "fal-ai" && isFalImageEditModel(parsed.model)) || + providerConfig.id === "openrouter" + ) { + return parsed.provider ? { credKey: parsed.provider } : null; + } + // Other built-in providers do not expose an OpenAI-compatible edit endpoint. + return null; + } + // Custom OpenAI-compatible node: prefix already rewritten to `/model`. + const slash = resolvedModel.indexOf("/"); + if (slash > 0 && slash < resolvedModel.length - 1) { + return { credKey: resolvedModel.slice(0, slash) }; + } + return null; +} + +/** + * Dispatch a single edit-capable target with already-resolved credentials, and + * return a normalized {success,data,status,error}. Reuses the same provider + * handlers postHandler uses for the single-model path. + */ +async function dispatchImageEditTarget( + target: EditComboTarget, + credentials: unknown, + ctx: ImageEditContext +): Promise { + const { parsed, providerConfig, modelStr } = target; + const { prompt, size, responseFormat, images, imageBytes, imageMime, request } = ctx; + + // Built-in Codex — native Responses hosted tool for reference-image edits. + if (providerConfig?.format === "codex-responses") { + const modelEntry = getImageModelEntry(modelStr); + if (!modelEntry || modelEntry.provider !== "codex" || modelEntry.model !== parsed.model) { + return { success: false, status: HTTP_STATUS.BAD_REQUEST, error: `Unsupported Codex image edit model: ${modelStr}` }; + } + const imageValidationError = validateCodexImageEditReferences(images); + if (imageValidationError) { + return { success: false, status: HTTP_STATUS.BAD_REQUEST, error: imageValidationError }; + } + const credentialDetails = credentials as { + connectionId?: unknown; + providerSpecificData?: unknown; + }; + if (isCodexFreePlan(credentialDetails.providerSpecificData)) { + return { + success: false, + status: HTTP_STATUS.BAD_REQUEST, + error: "Codex image editing requires a paid ChatGPT/Codex plan", + }; + } + const connectionId = + typeof credentialDetails.connectionId === "string" ? credentialDetails.connectionId : null; + let proxyInfo = null; + if (connectionId) { + try { + proxyInfo = await resolveProxyForConnection(connectionId); + } catch { + log.debug("PROXY", `Failed to resolve proxy for image provider: ${parsed.provider}`); + } + } + const editImage = () => + handleCodexImageEdit({ + provider: parsed.provider, + model: parsed.model, + providerConfig, + body: { + prompt, + size: size ?? undefined, + response_format: responseFormat ?? undefined, + }, + referenceImages: images, + credentials: credentials as never, + log, + signal: request.signal, + }); + return (await (connectionId + ? runWithProxyContext(proxyInfo?.proxy || null, editImage).catch(() => ({ + success: false as const, + status: HTTP_STATUS.SERVICE_UNAVAILABLE, + error: "Image edit proxy error", + })) + : editImage())) as ImageComboDispatchResult; + } + + if (providerConfig?.format === "fal-ai" && isFalImageEditModel(parsed.model)) { + return (await handleFalAIImageEdit({ + provider: parsed.provider, + model: parsed.model, + providerConfig, + body: { prompt, size: size ?? undefined, response_format: responseFormat ?? undefined, n: 1 }, + images, + credentials: credentials as never, + log, + })) as ImageComboDispatchResult; + } + + if (providerConfig?.format === "adobe-firefly-image") { + const dataUrls = buildAdobeFireflyEditDataUrls(images, imageBytes, imageMime); + if (dataUrls.length === 0) { + return { success: false, status: HTTP_STATUS.BAD_REQUEST, error: "Missing required field: image" }; + } + return (await handleAdobeFireflyImageGeneration({ + provider: parsed.provider, + model: parsed.model, + providerConfig, + body: { + prompt, + size: size ?? undefined, + response_format: responseFormat ?? undefined, + n: 1, + image_url: dataUrls[0], + image: dataUrls.length === 1 ? dataUrls[0] : dataUrls, + image_urls: dataUrls, + images: dataUrls, + }, + credentials: credentials as never, + log, + })) as ImageComboDispatchResult; + } + + if (providerConfig?.id === "openrouter") { + return (await handleOpenRouterImageEdit({ + provider: parsed.provider, + model: parsed.model, + baseUrl: providerConfig.baseUrl, + credentials: credentials as never, + prompt, + imageBytes, + imageMime, + size: size ?? undefined, + n: 1, + log, + })) as ImageComboDispatchResult; + } + + // Custom OpenAI-compatible node: forward to {base_url}/images/edits. + const slash = modelStr.indexOf("/"); + const customProviderId = slash > 0 ? modelStr.slice(0, slash) : null; + const customModel = slash > 0 ? modelStr.slice(slash + 1) : null; + if (!customProviderId || !customModel) { + return { + success: false, + status: HTTP_STATUS.BAD_REQUEST, + error: `Unknown image provider for model "${modelStr}"`, + }; + } + return (await handleOpenAIImageEdit({ + provider: customProviderId, + model: customModel, + credentials: credentials as never, + prompt, + imageBytes, + imageMime, + size, + responseFormat, + n: 1, + log, + })) as ImageComboDispatchResult; +} + +/** + * #12547: run an image-edit request whose model is a bare combo/alias name over + * the combo's edit-capable targets, mirroring how /v1/images/generations diverts + * bare combos to executeImageCombo (#9239). A combo whose first target isn't + * edit-capable (or lacks credentials) now falls through to a later edit-capable + * target instead of flattening to the first target and hard-erroring. + */ +async function executeImageEditCombo(comboName: string, ctx: ImageEditContext): Promise { + const combo = await getComboByName(comboName); + if (!combo) { + return errorResponse(HTTP_STATUS.BAD_REQUEST, `Combo not found: ${comboName}`); + } + const allCombos = await getCombos(); + const targets = resolveComboTargets(combo as never, allCombos as never); + if (!targets || targets.length === 0) { + return errorResponse(HTTP_STATUS.BAD_REQUEST, `Combo "${comboName}" has no usable targets`); + } + + // Build the edit-capable target list (prefix-resolved). Non-edit-capable and + // retired targets are skipped here so the loop only iterates dispatchable ones. + const editTargets: EditComboTarget[] = []; + for (const t of targets) { + const raw = + typeof (t as { modelStr?: unknown }).modelStr === "string" + ? ((t as { modelStr: string }).modelStr as string) + : ""; + if (!raw.trim()) continue; + let resolved: string; + try { + resolved = await resolveImageModelPrefix(raw); + } catch { + // retired provider / prefix — skip this target + continue; + } + const parsed = parseImageModel(resolved); + const providerConfig = parsed.provider ? getImageProvider(parsed.provider) : null; + const capability = classifyImageEditTarget(resolved, parsed, providerConfig); + if (!capability) continue; + editTargets.push({ modelStr: resolved, parsed, providerConfig, credKey: capability.credKey }); + } + + if (editTargets.length === 0) { + return errorResponse( + HTTP_STATUS.BAD_REQUEST, + `No image-edit-capable targets in combo "${comboName}"` + ); + } + + const run = await runImageComboTargets(editTargets, { + resolveProvider: (target) => ({ provider: target.credKey, model: target.parsed.model }), + resolveCredentials: (_provider, target) => + getProviderCredentialsWithQuotaPreflight( + target.credKey, + null, + ctx.allowedConnections, + target.modelStr + ), + isRateLimited: isAllRateLimitedCredentials, + dispatch: ({ target, credentials }) => dispatchImageEditTarget(target, credentials, ctx), + onSuccess: async (credentials) => { + await clearRecoveredProviderState(credentials as never); + }, + failureLabel: "Image edit failed", + }); + + if (run.outcome === "terminal") { + return errorResponse(run.status, `[${run.provider}] ${run.error}`); + } + if (run.outcome === "success") { + // Match the single-model edit path: return the provider payload directly. + return jsonResponse(run.data); + } + const errorPayload = toJsonErrorPayload( + run.lastError?.error || "All combo targets failed", + "Image edit combo targets all failed" + ); + return new Response(JSON.stringify(errorPayload), { + status: run.lastError?.status || HTTP_STATUS.BAD_GATEWAY, + headers: { "Content-Type": "application/json" }, + }); +} + async function postHandler(request: Request, _context?: unknown) { let input: EditInput | null; try { @@ -345,6 +633,39 @@ async function postHandler(request: Request, _context?: unknown) { const fullModel = model; + // #12547: a bare combo/alias name iterates the combo's edit-capable targets + // (mirrors generations' #9239 diversion, which runs before resolveImageRouteModel). + // Without this, resolveImageRouteModel flattens the combo to its first target, so a + // combo whose first target isn't edit-capable hard-errors even when a later target is. + if (!fullModel.includes("/")) { + let combo: unknown = null; + try { + combo = await getComboByName(fullModel); + } catch { + combo = null; + } + if (combo) { + const comboPolicy = await enforceApiKeyPolicy(request, fullModel); + if (comboPolicy.rejection) return comboPolicy.rejection; + const comboAllowedConnections = + comboPolicy.apiKeyInfo?.allowedConnections && + comboPolicy.apiKeyInfo.allowedConnections.length > 0 + ? comboPolicy.apiKeyInfo.allowedConnections + : null; + return executeImageEditCombo(fullModel, { + prompt, + size, + responseFormat, + images, + imageBytes, + imageMime, + imageInputCount, + allowedConnections: comboAllowedConnections, + request, + }); + } + } + // Resolve combo/alias, custom-provider prefix, and built-in ids consistently with // /v1/images/generations (#3215). Retirement is resolved before API-key policy // so the same explicit provider request always receives the deterministic 410. diff --git a/tests/unit/image-combo-edits-fallback-12547.test.ts b/tests/unit/image-combo-edits-fallback-12547.test.ts new file mode 100644 index 0000000000..16054db7ce --- /dev/null +++ b/tests/unit/image-combo-edits-fallback-12547.test.ts @@ -0,0 +1,219 @@ +// #12547 (diegosouzapw endorsed): /v1/images/edits must iterate a combo's targets +// the same way /v1/images/generations does (#9239), so a combo whose FIRST target +// isn't edit-capable (or lacks credentials) falls through to a later edit-capable +// target instead of flattening to the first target and hard-erroring. +// +// Before this change: /v1/images/edits resolved a bare combo name to its first +// target via resolveSingleImageComboTarget() and dispatched only that one. A combo +// like ["openai/gpt-image-2", "openrouter/..."] hard-errored ("Image edit is not +// supported for built-in provider openai") even though the OpenRouter target could +// have serviced the edit. Missing credentials on the first target were likewise a +// hard 401 for the whole request. +// +// After this change: the edits route diverts bare combos through the same shared +// runImageComboTargets loop generations uses, filtered to edit-capable targets. +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-image-combo-edits-12547-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "image-combo-edits-12547-secret"; +process.env.JWT_SECRET = process.env.JWT_SECRET || "image-combo-edits-12547-jwt"; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); +const combosDb = await import("../../src/lib/db/combos.ts"); +const imageEditRoute = await import("../../src/app/api/v1/images/edits/route.ts"); +const { executeImageCombo } = await import("../../open-sse/services/imageCombo.ts"); +const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); + +interface ErrorResponseBody { + error: { message: string; code?: string }; +} +interface ImageResponseBody { + data: Array<{ b64_json?: string; url?: string }>; +} + +const originalFetch = globalThis.fetch; + +async function resetStorage() { + globalThis.fetch = originalFetch; + apiKeysDb.resetApiKeyState(); + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + v1ModelsCatalog.__resetCatalogBuilderRunsForTest(); +} + +function seedOpenRouterConnection() { + return providersDb.createProviderConnection({ + provider: "openrouter", + authType: "apikey", + name: "openrouter-combo-edit", + apiKey: "sk-or-combo-edit-12547", + isActive: true, + testStatus: "active", + rateLimitedUntil: null, + }); +} + +function dataUrlPng(bytes: number[]): string { + return `data:image/png;base64,${Buffer.from(bytes).toString("base64")}`; +} + +const REF_A = dataUrlPng([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1]); + +function editRequest(model: string, images: string[] = [REF_A]): Request { + return new Request("http://localhost/api/v1/images/edits", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model, prompt: "add a red hat", images }), + }); +} + +/** Mock a successful OpenRouter unified-Image-API edit response. */ +function mockOpenRouterSuccess(): void { + globalThis.fetch = async () => + new Response( + JSON.stringify({ + data: [{ b64_json: Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString("base64") }], + }), + { status: 200, headers: { "content-type": "application/json" } } + ); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(() => { + globalThis.fetch = originalFetch; + apiKeysDb.resetApiKeyState(); + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +// --------------------------------------------------------------------------- +// Discriminant #1 — first target is NOT edit-capable, a later one is. +// RED on base (400 "not supported for built-in provider openai"); GREEN with fix. +// --------------------------------------------------------------------------- +test("#12547 edits combo falls through a non-edit-capable first target to a later one", async () => { + await seedOpenRouterConnection(); + mockOpenRouterSuccess(); + await combosDb.createCombo({ + name: "edit-fallback-combo", + strategy: "priority", + // openai/gpt-image-2 is a built-in provider with NO OpenAI-compatible edit + // endpoint (the single-model path hard-errors on it); the openrouter target can edit. + models: ["openai/gpt-image-2", "openrouter/google/gemini-3.1-flash-image-preview"], + }); + + const response = await imageEditRoute.POST(editRequest("edit-fallback-combo")); + const body = (await response.json()) as ImageResponseBody; + + assert.equal(response.status, 200, "must fall through to the edit-capable openrouter target"); + assert.ok(body.data?.[0]?.b64_json, "edit returns an image payload from the later target"); +}); + +// --------------------------------------------------------------------------- +// Discriminant #2 — first target IS edit-capable but lacks credentials. +// Matching generations, missing credentials is a SKIP (not a hard 401). A later +// credentialed target services the edit. +// RED on base (401 "No credentials for provider: codex"); GREEN with fix. +// --------------------------------------------------------------------------- +test("#12547 edits combo skips an edit-capable first target missing credentials", async () => { + await seedOpenRouterConnection(); // only openrouter is credentialed; codex is not + mockOpenRouterSuccess(); + await combosDb.createCombo({ + name: "edit-skip-nocreds-combo", + strategy: "priority", + models: ["codex/gpt-5.6-sol", "openrouter/google/gemini-3.1-flash-image-preview"], + }); + + const response = await imageEditRoute.POST(editRequest("edit-skip-nocreds-combo")); + const body = (await response.json()) as ImageResponseBody; + + assert.equal(response.status, 200, "missing creds on the first target must skip, not 401"); + assert.ok(body.data?.[0]?.b64_json, "edit returns an image payload from the credentialed target"); +}); + +// --------------------------------------------------------------------------- +// Guard — a combo with no edit-capable target reports a clear 400 (no stack leak). +// --------------------------------------------------------------------------- +test("#12547 edits combo with no edit-capable targets returns a clean 400", async () => { + globalThis.fetch = async () => { + throw new Error("No edit-capable target must never reach upstream"); + }; + await combosDb.createCombo({ + name: "no-edit-capable-combo", + strategy: "priority", + // openai + a chat model: neither exposes an OpenAI-compatible edit endpoint. + models: ["openai/gpt-image-2", "openai/gpt-4o"], + }); + + const response = await imageEditRoute.POST(editRequest("no-edit-capable-combo")); + const body = (await response.json()) as ErrorResponseBody; + + assert.equal(response.status, 400); + assert.match(body.error.message, /No image-edit-capable targets/); + assert.ok(!body.error.message.includes("at /"), "no stack trace leak"); +}); + +// --------------------------------------------------------------------------- +// /v1/images/generations behavior is unchanged by the shared-loop extraction. +// The generation combo path still filters non-image targets and reports the +// image-capable-but-uncredentialed error (not the filtering error). +// --------------------------------------------------------------------------- +function createLog() { + const record = () => () => 0; + return { info: record(), warn: record(), error: record(), debug: record() }; +} + +test("#12547 generations combo still rejects a chat-only combo with 'No images-capable targets'", async () => { + await combosDb.createCombo({ + name: "gen-chat-only-combo", + strategy: "priority", + models: ["openai/gpt-4o"], + }); + + const response = await executeImageCombo( + "gen-chat-only-combo", + { model: "gen-chat-only-combo", prompt: "a cat" }, + { + request: new Request("http://localhost/v1/images/generations", { method: "POST" }), + policy: { apiKeyInfo: { id: "k", name: "k" } }, + }, + Date.now(), + createLog() as never + ); + assert.equal(response.status, 400); + const body = (await response.json()) as ErrorResponseBody; + assert.match(JSON.stringify(body), /No images-capable targets/); +}); + +test("#12547 generations combo still surfaces missing credentials for image targets", async () => { + await combosDb.createCombo({ + name: "gen-img-no-conn-combo", + strategy: "priority", + models: ["openai/gpt-image-2", "openai/gpt-image-1.5"], + }); + + const response = await executeImageCombo( + "gen-img-no-conn-combo", + { model: "gen-img-no-conn-combo", prompt: "a cat", n: 1 }, + { + request: new Request("http://localhost/v1/images/generations", { method: "POST" }), + policy: { apiKeyInfo: { id: "k", name: "k" } }, + }, + Date.now(), + createLog() as never + ); + assert.equal(response.status, 400); + const body = (await response.json()) as ErrorResponseBody; + // Image-capable targets were found (so NOT the filtering error); the failure is credentials. + assert.ok(!JSON.stringify(body).includes("No images-capable targets")); +}); From 616d54cf1969ba8024c1f57348ad275be0b48dda Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:37 +0200 Subject: [PATCH 04/20] fix(config): persist background-degradation entry deletions (#12647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tell is convincing: `detectionPatterns` in the same function already treats a present stored value as authoritative, so the two halves of one object disagreed. Making `degradationMap` stored-authoritative-when-present is the smaller change and the consistent one. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12647-background-degradation-deletions.md | 1 + src/lib/config/runtimeSettings.ts | 9 +-- ...ground-degradation-deletions-12424.test.ts | 57 +++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixes/12647-background-degradation-deletions.md create mode 100644 tests/unit/settings/background-degradation-deletions-12424.test.ts diff --git a/changelog.d/fixes/12647-background-degradation-deletions.md b/changelog.d/fixes/12647-background-degradation-deletions.md new file mode 100644 index 0000000000..5339d07594 --- /dev/null +++ b/changelog.d/fixes/12647-background-degradation-deletions.md @@ -0,0 +1 @@ +- **fix(config):** Persist deletions of built-in background-degradation entries — when a stored settings record exists its `degradationMap` is now authoritative instead of being merged under the defaults, so an entry the user removed in the dashboard no longer reappears on the next apply or restart ([#12424](https://github.com/diegosouzapw/OmniRoute/issues/12424)) diff --git a/src/lib/config/runtimeSettings.ts b/src/lib/config/runtimeSettings.ts index 0cb93c8fee..ed5930cbb7 100644 --- a/src/lib/config/runtimeSettings.ts +++ b/src/lib/config/runtimeSettings.ts @@ -323,10 +323,11 @@ async function applyBackgroundDegradationSection(backgroundDegradation: JsonReco setBackgroundDegradationConfig({ enabled: backgroundDegradation.enabled === true, - degradationMap: { - ...getDefaultDegradationMap(), - ...normalizeStringRecord(backgroundDegradation.degradationMap), - }, + // #12424: a present stored record is authoritative for degradationMap — do NOT back-fill + // defaults, or a key the user deleted (absent from the stored map) resurrects on every + // apply/restart. Mirrors detectionPatterns below, which already treats a present stored + // value as authoritative and only falls back to defaults when it is empty. + degradationMap: normalizeStringRecord(backgroundDegradation.degradationMap), detectionPatterns: normalizeStringArray(backgroundDegradation.detectionPatterns).length > 0 ? normalizeStringArray(backgroundDegradation.detectionPatterns) diff --git a/tests/unit/settings/background-degradation-deletions-12424.test.ts b/tests/unit/settings/background-degradation-deletions-12424.test.ts new file mode 100644 index 0000000000..f5b157a723 --- /dev/null +++ b/tests/unit/settings/background-degradation-deletions-12424.test.ts @@ -0,0 +1,57 @@ +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"; + +process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-bgdeg-12424-")); + +const { applyRuntimeSettings, resetRuntimeSettingsStateForTests } = await import( + "../../../src/lib/config/runtimeSettings.ts" +); +const { + getBackgroundDegradationConfig, + getDefaultDegradationMap, + getDefaultDetectionPatterns, + setBackgroundDegradationConfig, +} = await import("../../../open-sse/services/backgroundTaskDetector.ts"); + +// Issue #12424: deleting a built-in background-degradation entry through the dashboard +// did not persist — the runtime loader merged defaults *under* the stored map, so a key +// the user removed (absent from the stored record) was indistinguishable from one never +// touched and always came back on the next apply/restart. +test("stored degradationMap that omits a default key does not resurrect it (#12424)", async () => { + resetRuntimeSettingsStateForTests(); + setBackgroundDegradationConfig({ + enabled: false, + degradationMap: getDefaultDegradationMap(), + detectionPatterns: getDefaultDetectionPatterns(), + }); + + const defaults = getDefaultDegradationMap(); + const deletedKey = "gpt-5"; + const keptKey = "gpt-4o"; + assert.ok( + defaults[deletedKey] && defaults[keptKey], + "fixture assumes these default keys exist in DEFAULT_DEGRADATION_MAP" + ); + + // The stored map is every default except the one the user deleted. + const stored: Record = { ...defaults }; + delete stored[deletedKey]; + + await applyRuntimeSettings( + { backgroundDegradation: JSON.stringify({ enabled: true, degradationMap: stored }) }, + { force: true, source: "test" } + ); + + const applied = getBackgroundDegradationConfig().degradationMap; + + // The entries the user kept still apply… + assert.equal(applied[keptKey], defaults[keptKey], "a kept default entry still applies"); + // …and the one they deleted stays deleted instead of being back-filled from defaults. + assert.ok( + !(deletedKey in applied), + `deleted default '${deletedKey}' must not be re-added from defaults` + ); +}); From 2e10346f45f8bb108605abfef3f3c7616af04b54 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:43 +0200 Subject: [PATCH 05/20] docs(reference): sync FEATURE_FLAGS.md with featureFlagDefinitions (#12552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doc promised a 1:1 catalog and had drifted to 20 missing keys, two rows that were not flags at all, and a port number that disagreed with the code. Good call adding a static sync test — it caught its own drift immediately: I added the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after you wrote this. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12552-feature-flags-reference-sync.md | 1 + docs/reference/FEATURE_FLAGS.md | 133 +++++++++++------- scripts/check/check-fabricated-docs.mjs | 24 +++- .../feature-flags-doc-sync-static.test.ts | 100 +++++++++++++ 4 files changed, 199 insertions(+), 59 deletions(-) create mode 100644 changelog.d/fixes/12552-feature-flags-reference-sync.md create mode 100644 tests/unit/feature-flags-doc-sync-static.test.ts diff --git a/changelog.d/fixes/12552-feature-flags-reference-sync.md b/changelog.d/fixes/12552-feature-flags-reference-sync.md new file mode 100644 index 0000000000..62b599ac6d --- /dev/null +++ b/changelog.d/fixes/12552-feature-flags-reference-sync.md @@ -0,0 +1 @@ +- **docs(reference):** bring the `FEATURE_FLAGS.md` catalog back to 1:1 with `featureFlagDefinitions.ts` — 20 missing flags added, the two `*_BLOCK_THRESHOLD` env-only knobs moved out of the flag tables, category/total counts and the Live WS port corrected, guarded by a static test (#12552 — thanks @pacocartones) diff --git a/docs/reference/FEATURE_FLAGS.md b/docs/reference/FEATURE_FLAGS.md index f7e046d169..65a5bcac12 100644 --- a/docs/reference/FEATURE_FLAGS.md +++ b/docs/reference/FEATURE_FLAGS.md @@ -1,7 +1,7 @@ --- title: "Feature Flags" -version: 3.8.40 -lastUpdated: 2026-06-28 +version: 3.8.51 +lastUpdated: 2026-09-03 --- # Feature Flags @@ -46,66 +46,85 @@ A boolean flag is considered **enabled** when its effective value is `"true"`, ## Flag Catalog -37 flags across 6 categories. **Default** is the definition default — the value +55 flags across 6 categories. **Default** is the definition default — the value used when neither a DB override nor an environment variable is present. -### Security (7) +### Security (10) -| Key | Type | Default | Description | -| --------------------------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------- | -| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. | -| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. | -| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. | -| `INPUT_SANITIZER_BLOCK_THRESHOLD` | enum | `high` | Minimum severity blocked when mode is `block` (`high`/`medium`/`low`). Medium families are observe-only at default. | -| `INJECTION_GUARD_BLOCK_THRESHOLD` | enum | _(unset)_ | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD`. | -| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). | -| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. | -| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. | -| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Block outbound requests to private/internal IP ranges. | +| Key | Type | Default | Description | +| --------------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. | +| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. | +| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. | +| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). | +| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. | +| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. | +| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Block outbound requests to private/internal IP ranges. | +| `ALLOW_API_KEY_REVEAL` | boolean | `false` | Allow authenticated dashboard users to reveal stored API keys instead of only seeing masked values. | +| `AUTH_LOG_INCLUDE_ACCOUNT_ID` | boolean | `false` | Include account prefix in AUTH log lines (e.g. "Using account: abc12345..."). Disabled by default so account identifiers are redacted from shared/multi-tenant process logs. Independent from Debug Mode; flipping Debug Mode does not reveal this. | +| `OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN` | boolean | `false` | When OIDC is enabled, disable password login so users can only authenticate via OIDC Single Sign-On. When disabled (default), both password login and OIDC are available. | -### Network (8) +### Network (9) -| Key | Type | Default | Restart | Description | -| ----------------------------------------------- | ------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ENABLE_TLS_FINGERPRINT` | boolean | `false` | ✓ | Enable TLS fingerprint stealth mode. | -| `PROXY_AUTO_SELECT_ENABLED` | boolean | `false` | | When no proxy is assigned to a connection, auto-select the first working proxy from the registry. Off by default (otherwise any registry proxy becomes a global fallback — #3332). | -| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | boolean | `false` | | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Off by default because this can change egress IP. | -| `MITM_DISABLE_TLS_VERIFY` | boolean | `false` | ✓ | Disable TLS certificate verification for the MITM proxy. **Danger.** | -| `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` | boolean | `false` | | Allow provider URLs pointing to private/internal networks. | -| `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` | boolean | `true` | | Allow adding/validating providers on local/private addresses (127.0.0.1, localhost, LAN). On by default (local-first); disable for strict public-only blocking. Cloud-metadata stays blocked. | -| `ENABLE_CC_COMPATIBLE_PROVIDER` | boolean | `false` | ✓ | Enable Claude Code compatible provider mode. | +| Key | Type | Default | Restart | Description | +| ----------------------------------------------- | ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ENABLE_TLS_FINGERPRINT` | boolean | `false` | ✓ | Enable TLS fingerprint stealth mode. | +| `AUDIO_REMOTE_PROVIDER_NODES` | boolean | `false` | | Allow the /v1/audio/* routes to use OpenAI-compatible provider nodes hosted outside localhost. Off by default — routing audio to a remote host changes egress identity and must be an explicit operator decision. Loopback nodes are always allowed and unaffected. | +| `PROXY_AUTO_SELECT_ENABLED` | boolean | `false` | | When no proxy is assigned to a connection, auto-select the first working proxy from the registry. Off by default (otherwise any registry proxy becomes a global fallback — #3332). | +| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | boolean | `false` | | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Off by default because this can change egress IP. | +| `NETWORK_ROTATION_SHARED_EGRESS_GUARD` | boolean | `true` | | On a network exception (timeout, connection refused/reset) for a multi-account rotation executor, when the failing account has no dedicated proxy, apply a short cooldown and skip other proxy-less accounts for the rest of the request instead of retrying each one. On by default (safe: no egress IP change, only reduces latency/cooldown risk on shared-egress accounts). Disable to restore immediate propagation on the first proxy-less throw. | +| `MITM_DISABLE_TLS_VERIFY` | boolean | `false` | ✓ | Disable TLS certificate verification for the MITM proxy. **Danger.** | +| `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` | boolean | `false` | | Allow provider URLs pointing to private/internal networks. | +| `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` | boolean | `true` | | Allow adding/validating providers on local/private addresses (127.0.0.1, localhost, LAN). On by default (local-first); disable for strict public-only blocking. Cloud-metadata stays blocked. | +| `ENABLE_CC_COMPATIBLE_PROVIDER` | boolean | `false` | ✓ | Enable Claude Code compatible provider mode. | -### Policies (3) +### Policies (5) -| Key | Type | Default | Restart | Description | -| ------------------------------- | ------- | ---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `TOOL_POLICY_MODE` | enum | `disabled` | | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. | -| `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | | Automatically enable rate limiting based on usage patterns. | -| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. | +| Key | Type | Default | Description | +| ------------------------------- | ------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TOOL_POLICY_MODE` | enum | `disabled` | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. | +| `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | Automatically enable rate limiting based on usage patterns. | +| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. | +| `CAPABILITY_FILTER_ENABLED` | boolean | `false` | Reject requests before dispatch when the target model lacks required capabilities (vision, tools, structured output, context window). Protects direct single-provider requests that bypass the combo-layer compatibility filter. | +| `RADAR_ENABLED` | boolean | `false` | Enable the OmniRoute Radar module (catalog feed screens and sync). Off by default; enabling only unlocks the UI — data sync remains a separate opt-in. | -### Runtime (11) +### Runtime (23) -| Key | Type | Default | Restart | Description | -| ------------------------------------------- | ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `EXPOSE_CC_DISCOVERY_ALIASES` | boolean | `false` | | Advertise `claude//` mirror ids on `/v1/models` so Claude Code gateway model discovery lists non-Claude models. Global level of the three-level gate (env wins over the dashboard override). See [Claude Code configuration](../guides/CLAUDE-CODE-CONFIGURATION.md#discovery-aliases--surface-non-claude-models-in-the-model-picker). | -| `OMNIROUTE_MCP_ENFORCE_SCOPES` | boolean | `true` | | Enforce scope restrictions on MCP tool access. | -| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | boolean | `false` | | Compress MCP tool descriptions to reduce token usage. | -| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | boolean | `false` | | Enable background task processing at runtime. | -| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | boolean | `false` | ✓ | Disable all background services (quota refresh, sync, etc). | -| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | boolean | `false` | | Trust project-level RTK filters without validation. | -| `OMNIROUTE_ENABLE_LIVE_WS` | boolean | `true` | ✓ | Start the real-time dashboard WebSocket server on import (port 20129 by default). | -| `OMNIROUTE_CODEX_WS_ENABLED` | boolean | `true` | | Allow Codex to use the Responses-over-WebSocket transport. When off, Codex falls back to HTTP Responses. | -| `OMNIROUTE_EMERGENCY_FALLBACK` | boolean | `true` | | Route budget-exhausted requests to the emergency free fallback provider/model. (See [Emergency Budget Fallback](#emergency-budget-fallback) below.) | -| `MODEL_CATALOG_INCLUDE_NAMES` | boolean | `true` | | Include display-friendly name fields in `/v1/models` responses. Disable for clients that expect model IDs only. | -| `ARENA_ELO_SYNC_ENABLED` | boolean | `true` | | Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings. | +| Key | Type | Default | Restart | Description | +| ------------------------------------------- | ------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UNIVERSAL_CONTEXT_HANDOFF_ENABLED` | boolean | `true` | | Generate and inject conversation summaries when combo routing switches models. Disable to treat model switches independently and prevent background handoff requests for all existing and future combos. | +| `RESPONSES_PASSTHROUGH_DROP_COMMENTARY` | boolean | `true` | | Drop internal commentary-phase output items from Responses API passthrough streams before forwarding to clients. Disable to receive raw upstream commentary. | +| `OMNIROUTE_MCP_ENFORCE_SCOPES` | boolean | `true` | | Enforce scope restrictions on MCP tool access. | +| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | boolean | `false` | | Compress MCP tool descriptions to reduce token usage. | +| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | boolean | `false` | | Enable background task processing at runtime. | +| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | boolean | `false` | ✓ | Disable all background services (quota refresh, sync, etc). | +| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | boolean | `false` | | Trust project-level RTK filters without validation. | +| `OMNIROUTE_ENABLE_LIVE_WS` | boolean | `true` | ✓ | Start the real-time dashboard WebSocket server on import (port 20132 by default). | +| `OMNIROUTE_CODEX_WS_ENABLED` | boolean | `true` | | Allow Codex to use the Responses-over-WebSocket transport. When off, Codex falls back to HTTP Responses. | +| `OMNIROUTE_CODEX_APP_SERVER_ENABLED` | boolean | `true` | | Allow Codex to use the local app-server WebSocket JSON-RPC transport (codexTransport=app-server). When off, connections opted into app-server fall back to Codex's other transports. | +| `OMNIROUTE_EMERGENCY_FALLBACK` | boolean | `true` | | Route budget-exhausted requests to the emergency free fallback provider/model. (See [Emergency Budget Fallback](#emergency-budget-fallback) below.) | +| `STREAM_RECOVERY_ENABLED` | boolean | `false` | | Enable transparent early retry for truncated upstream SSE streams before any response bytes reach the client. | +| `STREAM_RECOVERY_MIDSTREAM_ENABLED` | boolean | `false` | | Allow stream recovery to re-request and stitch a response after bytes have already reached the client. | +| `MODEL_CATALOG_INCLUDE_NAMES` | boolean | `true` | | Include display-friendly name fields in `/v1/models` responses. Disable for clients that expect model IDs only. | +| `MODELS_CATALOG_PREFIX_MODE` | enum | `dual` | | Controls how model IDs are prefixed in /v1/models. 'dual' (default) emits both alias and canonical provider-id prefixes for backward compatibility. 'alias' emits only the short alias prefix (e.g. ds-web/model, not deepseek-web/model). 'canonical' emits only the full provider-id prefix. Values: `dual`, `alias`, `canonical`. | +| `ARENA_ELO_SYNC_ENABLED` | boolean | `true` | | Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings. | +| `EXPOSE_CC_DISCOVERY_ALIASES` | boolean | `false` | | Advertise `claude//` mirror ids on `/v1/models` so Claude Code gateway model discovery lists non-Claude models. Global level of the three-level gate (env wins over the dashboard override). See [Claude Code configuration](../guides/CLAUDE-CODE-CONFIGURATION.md#discovery-aliases--surface-non-claude-models-in-the-model-picker). | +| `NO_THINKING_ALIAS_ENABLED` | boolean | `true` | | Master switch for the no-think// gateway aliases. On (default): /v1/models advertises a no-thinking variant for every eligible thinking-capable Claude model, and a no-think/ id sent on a request resolves back to the real model with reasoning suppressed. Off: no variants are advertised and a no-think/ id is treated like any other unknown model id. The per-model ModelSpec.noThinkingAlias opt-in/opt-out still applies while this is on. | +| `OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS` | boolean | `false` | | Disable the generation of thinking level variants (e.g. -low, -medium, -high) in the /v1/models catalog. | +| `OMNIROUTE_CHAT_VIRTUAL_LANES` | boolean | `false` | ✓ | Enable per-tenant adaptive virtual admission lanes for provider dispatch (#9654): one tenant's burst no longer 503s another. The OMNIROUTE_CHAT_VIRTUAL_LANES env var wins over this dashboard override; changes take effect at server restart. | +| `EXPOSE_FUNCTIONAL_GATEWAY_MIRRORS` | boolean | `false` | | Advertise / mirror ids on /v1/models for models whose canonical owner has no active credential but a passthrough gateway with an active credential routes them. Warning: adds catalog entries for all clients when enabled globally. | +| `NEWAPI_AGGREGATOR_BALANCE` | boolean | `false` | | Enable balance detection for New-API / One-API / Sub2API aggregator compatible nodes. When enabled, compatible nodes with the aggregator flag set will report their balance in the dashboard and quota-preflight routing. | +| `SERVER_OWNED_TOOL_LOOP_ENABLED` | boolean | `false` | | Continue non-streaming server-owned tool calls until the model returns a client-usable response. | -### CLI (3) +### CLI (5) -| Key | Type | Default | Restart | Description | -| ---------------------------- | ------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------- | -| `CLI_COMPAT_ALL` | boolean | `false` | ✓ | Enable compatibility mode for all CLI clients. | -| `MODEL_ALIAS_COMPAT_ENABLED` | boolean | `false` | | Enable model alias compatibility layer. | -| `PRICING_SYNC_ENABLED` | boolean | `false` | | Enable automatic pricing data synchronization (also requires the `PRICING_SYNC_ENABLED` environment variable). | +| Key | Type | Default | Restart | Description | +| ------------------------------------- | ------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CLI_COMPAT_ALL` | boolean | `false` | ✓ | Enable compatibility mode for all CLI clients. | +| `MODEL_ALIAS_COMPAT_ENABLED` | boolean | `false` | | Enable model alias compatibility layer. | +| `PRICING_SYNC_ENABLED` | boolean | `false` | | Enable automatic pricing data synchronization (also requires the `PRICING_SYNC_ENABLED` environment variable). | +| `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES` | boolean | `false` | | After a provider model sync, automatically (re)write ~/.codex/*.config.toml profile files from the live catalog. Never changes the active/default Codex config. Off by default. | +| `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` | boolean | `false` | | After a provider model sync, automatically (re)write ~/.claude/profiles//settings.json Claude Code profiles from the live catalog. Never changes the active/default Claude config. Off by default. | ### Health (3) @@ -115,6 +134,14 @@ used when neither a DB override nor an environment variable is present. | `OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK` | boolean | `false` | Disable the token validation health check. | | `SKILLS_SANDBOX_NETWORK_ENABLED` | boolean | `false` | Enable network access in the skills sandbox environment. | +> [!NOTE] +> `INPUT_SANITIZER_BLOCK_THRESHOLD` and its legacy alias +> `INJECTION_GUARD_BLOCK_THRESHOLD` tune the `block` mode of +> `INJECTION_GUARD_MODE`, but they are plain environment variables read by +> [`src/shared/utils/injectionSeverity.ts`](../../src/shared/utils/injectionSeverity.ts), +> not feature flags: they have no DB override and no dashboard toggle. See +> [`ENVIRONMENT.md`](./ENVIRONMENT.md#4-security--authentication). + > [!NOTE] > The `Restart` column marks flags with `requiresRestart: true` — the value is > persisted instantly but only takes effect after the process reloads. Enum @@ -168,10 +195,10 @@ Returns every flag with its effective value, source, and a summary. "requiresRestart": false, "warningLevel": "caution", }, - // ... all 33 flags + // ... all 55 flags ], "summary": { - "total": 33, + "total": 54, "active": 0, "inactive": 0, "overriddenByDb": 0, diff --git a/scripts/check/check-fabricated-docs.mjs b/scripts/check/check-fabricated-docs.mjs index 567a9610da..c74965a8cb 100644 --- a/scripts/check/check-fabricated-docs.mjs +++ b/scripts/check/check-fabricated-docs.mjs @@ -128,12 +128,6 @@ const ENV_VAR_ALLOWLIST = new Set([ "LINUX_GPG_KEY", // electron AppImage signing key, CI/build only (ELECTRON_GUIDE.md) "BRANCH_LOCK_TOKEN", // release branch-protection ops token (QUALITY_GATE_PLAYBOOK.md) "NEXT_LOCALE", // next-intl locale cookie name (I18N.md) - // Feature flags are resolved by key at runtime — `resolveFeatureFlag()` reads - // `process.env[key]` (src/shared/utils/featureFlags.ts), never a literal - // `process.env.MODELS_CATALOG_PREFIX_MODE`, so this scan cannot see the read. - // The flag is real: defined in featureFlagDefinitions.ts, overridable from the - // dashboard or the environment. (API_REFERENCE.md, VSCODE-COPILOT.md) - "MODELS_CATALOG_PREFIX_MODE", // Telegram Mini App integration (proposal TELEGRAM-MINIAPP.md, not yet implemented): env vars named in the feasibility analysis but no code reads them yet. "TELEGRAM_WEBHOOK_URL", // proposal-only: Telegram webhook public endpoint (TELEGRAM-MINIAPP.md, future feature) "TELEGRAM_WEBHOOK_SECRET", // proposal-only: Telegram webhook HMAC secret (TELEGRAM-MINIAPP.md, future feature) @@ -581,6 +575,24 @@ export function buildCodebaseIndex(root = ROOT) { } readEnvContract(); + // Feature flags are resolved by key at runtime — `resolveFeatureFlag()` reads + // `process.env[definition.key]` (src/shared/utils/featureFlags.ts), never a + // literal `process.env.`, so the code-read index cannot see those reads. + // Every key in FEATURE_FLAG_DEFINITIONS is therefore a real, env-overridable + // knob (docs/reference/FEATURE_FLAGS.md documents the catalog 1:1). + function readFeatureFlagContract() { + try { + const t = fs.readFileSync( + path.join(root, "src", "shared", "constants", "featureFlagDefinitions.ts"), + "utf8" + ); + for (const m of t.matchAll(/^\s*key:\s*"([A-Z][A-Z0-9_]+)"/gm)) envVars.add(m[1]); + } catch { + /* ignore */ + } + } + readFeatureFlagContract(); + // Set of `omniroute ` strings that exist in bin/ const cliCommands = new Set(); function walkCli(dir) { diff --git a/tests/unit/feature-flags-doc-sync-static.test.ts b/tests/unit/feature-flags-doc-sync-static.test.ts new file mode 100644 index 0000000000..8275529e38 --- /dev/null +++ b/tests/unit/feature-flags-doc-sync-static.test.ts @@ -0,0 +1,100 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { FEATURE_FLAG_DEFINITIONS } from "../../src/shared/constants/featureFlagDefinitions.ts"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, "..", ".."); + +/** + * docs/reference/FEATURE_FLAGS.md promises that its catalog matches + * FEATURE_FLAG_DEFINITIONS "1:1". Keep that promise checkable: every flag the + * code defines must be a table row with the same type and default, every table + * row must be a real flag, and the per-category / total counts must match. + */ +const doc = readFileSync(join(root, "docs/reference/FEATURE_FLAGS.md"), "utf8"); +const catalog = doc.slice(doc.indexOf("## Flag Catalog"), doc.indexOf("## Toggling Flags")); + +interface DocRow { + key: string; + type: string; + defaultValue: string; + restart: boolean; + category: string; +} + +function parseCatalog(): DocRow[] { + const rows: DocRow[] = []; + let category = ""; + for (const line of catalog.split("\n")) { + const heading = line.match(/^### (\w+) \(\d+\)/); + if (heading) { + category = heading[1].toLowerCase(); + continue; + } + const cells = line.match(/^\| `([A-Z0-9_]+)` +\| (\w+) +\| ([^|]+?) +\|(.*)$/); + if (!cells) continue; + rows.push({ + key: cells[1], + type: cells[2], + defaultValue: cells[3].replace(/`/g, ""), + restart: /^ *✓ *\|/.test(cells[4]), + category, + }); + } + return rows; +} + +const docRows = parseCatalog(); +const docByKey = new Map(docRows.map((row) => [row.key, row])); + +test("every defined feature flag has a catalog row in FEATURE_FLAGS.md", () => { + const missing = FEATURE_FLAG_DEFINITIONS.filter((d) => !docByKey.has(d.key)).map((d) => d.key); + assert.deepEqual( + missing, + [], + `flags defined in featureFlagDefinitions.ts but absent from the doc: ${missing.join(", ")}` + ); +}); + +test("every catalog row in FEATURE_FLAGS.md is a defined feature flag", () => { + const known = new Set(FEATURE_FLAG_DEFINITIONS.map((d) => d.key)); + const extra = docRows.filter((row) => !known.has(row.key)).map((row) => row.key); + assert.deepEqual( + extra, + [], + `doc rows that are not feature flags (env-only knobs belong in ENVIRONMENT.md): ${extra.join(", ")}` + ); +}); + +test("catalog rows carry the code's category, type, default and restart hint", () => { + const mismatches: string[] = []; + for (const def of FEATURE_FLAG_DEFINITIONS) { + const row = docByKey.get(def.key); + if (!row) continue; + if (row.category !== def.category) + mismatches.push(`${def.key}: category doc=${row.category} code=${def.category}`); + if (row.type !== def.type) mismatches.push(`${def.key}: type doc=${row.type} code=${def.type}`); + if (row.defaultValue !== def.defaultValue) + mismatches.push(`${def.key}: default doc=${row.defaultValue} code=${def.defaultValue}`); + if (row.restart !== def.requiresRestart) + mismatches.push(`${def.key}: requiresRestart doc=${row.restart} code=${def.requiresRestart}`); + } + assert.deepEqual(mismatches, []); +}); + +test("category headings and the total match the number of defined flags", () => { + const perCategory = new Map(); + for (const def of FEATURE_FLAG_DEFINITIONS) { + perCategory.set(def.category, (perCategory.get(def.category) ?? 0) + 1); + } + for (const [, name, count] of catalog.matchAll(/^### (\w+) \((\d+)\)/gm)) { + assert.equal(Number(count), perCategory.get(name.toLowerCase()), `heading count for ${name}`); + } + const total = catalog.match(/^(\d+) flags across (\d+) categories/m); + assert.ok(total, "expected an ' flags across categories' summary line"); + assert.equal(Number(total[1]), FEATURE_FLAG_DEFINITIONS.length, "total flag count"); + assert.equal(Number(total[2]), perCategory.size, "category count"); +}); From 3198c5414632ad31b95a686a30e1213d23410547 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:48 +0200 Subject: [PATCH 06/20] fix(orchestration): emit the real task status on non-status updates (#12550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct: `state: "updated"` mapped to no `OrchState`, so the channel carried a value no consumer could interpret. Reading the row back only on the no-status path, and publishing nothing when no row matched, both match what the A2A side already does. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12550-orchestration-emit-real-status.md | 1 + src/lib/cloudAgent/db.ts | 5 ++++- tests/unit/agents-channel-publish.test.ts | 21 ++++++++++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixes/12550-orchestration-emit-real-status.md diff --git a/changelog.d/fixes/12550-orchestration-emit-real-status.md b/changelog.d/fixes/12550-orchestration-emit-real-status.md new file mode 100644 index 0000000000..a0567e31f0 --- /dev/null +++ b/changelog.d/fixes/12550-orchestration-emit-real-status.md @@ -0,0 +1 @@ +- **fix(orchestration):** `updateCloudAgentTask` now publishes the task's real `status` on `agent.task.updated` when an update only touches `result`, `activities` or `error`, instead of the fabricated `"updated"` state, and stays silent when no row matched the id (#12550 — thanks @pacocartones) diff --git a/src/lib/cloudAgent/db.ts b/src/lib/cloudAgent/db.ts index 9d7f539078..7a93cef62e 100644 --- a/src/lib/cloudAgent/db.ts +++ b/src/lib/cloudAgent/db.ts @@ -121,7 +121,10 @@ export function updateCloudAgentTask( WHERE id = @id ` ).run({ id, ...validUpdates }); - emitAgentTaskUpdated("cloud-agent", id, (validUpdates.status as string) ?? "updated"); + // Publish the row's real status: an update that only touches result/activities/error must + // not fabricate a state the canvas has never heard of. No row means nothing was written. + const state = (validUpdates.status as string | undefined) ?? getCloudAgentTaskById(id)?.status; + if (state) emitAgentTaskUpdated("cloud-agent", id, state); } export function getCloudAgentTaskById(id: string): CloudAgentTaskRow | null { diff --git a/tests/unit/agents-channel-publish.test.ts b/tests/unit/agents-channel-publish.test.ts index 3c5f64df12..29dae16517 100644 --- a/tests/unit/agents-channel-publish.test.ts +++ b/tests/unit/agents-channel-publish.test.ts @@ -132,7 +132,9 @@ test("a throwing agent.task.updated listener does not break A2ATaskManager.creat // ── (b) cloud-agent DB writers ────────────────────────────────────────────────────────── -function makeTaskRow(overrides: Partial[0]> = {}) { +function makeTaskRow( + overrides: Partial[0]> = {} +) { const now = new Date().toISOString(); return { id: `task-${Math.random().toString(36).slice(2)}`, @@ -192,9 +194,10 @@ test("updateCloudAgentTask emits agent.task.updated with the new status", () => } }); -test("updateCloudAgentTask without a status field emits state 'updated'", () => { +test("updateCloudAgentTask without a status field emits the row's current status", () => { const row = makeTaskRow({ status: "queued" }); cloudAgentDb.insertCloudAgentTask(row); + cloudAgentDb.updateCloudAgentTask(row.id, { status: "running" }); const events: AgentTaskUpdatedPayload[] = []; const unsubscribe = on("agent.task.updated", (payload) => events.push(payload)); @@ -204,7 +207,19 @@ test("updateCloudAgentTask without a status field emits state 'updated'", () => assert.equal(events.length, 1); assert.equal(events[0].source, "cloud-agent"); assert.equal(events[0].taskId, row.id); - assert.equal(events[0].state, "updated"); + assert.equal(events[0].state, "running"); + } finally { + unsubscribe(); + } +}); + +test("updateCloudAgentTask on an unknown id does not emit (nothing was written)", () => { + const events: AgentTaskUpdatedPayload[] = []; + const unsubscribe = on("agent.task.updated", (payload) => events.push(payload)); + try { + cloudAgentDb.updateCloudAgentTask("task-does-not-exist", { result: "partial output" }); + + assert.equal(events.length, 0); } finally { unsubscribe(); } From 02884ed8d2a9d6ff1a25e49e414fa5a97af82046 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:53 +0200 Subject: [PATCH 07/20] fix(db): auto-clean conversation_turn_nodes and orphaned conversations (#12548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tables with no retention path at all and 775 MB of a 1.1 GB database is a real operational failure. Tying them to the existing `retention.callLogs` window rather than inventing a knob is right, and the reasoning is what makes it safe: once `cleanupCallLogs` purges the row `last_correlation_id` points at, the node can never render again. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- ...-db-cleanup-orphaned-conversation-nodes.md | 1 + .../api/settings/purge-usage-history/route.ts | 2 + src/lib/db/cleanup.ts | 128 +++++++++++- src/lib/db/cleanup/usagePurge.ts | 61 ++++-- ...b-cleanup-conversation-nodes-12453.test.ts | 190 ++++++++++++++++++ tests/unit/usage-history-reset.test.ts | 59 ++++++ 6 files changed, 421 insertions(+), 20 deletions(-) create mode 100644 changelog.d/fixes/12548-db-cleanup-orphaned-conversation-nodes.md create mode 100644 tests/unit/db-cleanup-conversation-nodes-12453.test.ts diff --git a/changelog.d/fixes/12548-db-cleanup-orphaned-conversation-nodes.md b/changelog.d/fixes/12548-db-cleanup-orphaned-conversation-nodes.md new file mode 100644 index 0000000000..1a5a015518 --- /dev/null +++ b/changelog.d/fixes/12548-db-cleanup-orphaned-conversation-nodes.md @@ -0,0 +1 @@ +- **fix(db):** Add `conversation_turn_nodes` and orphaned `agentic_conversations` to the auto-cleanup cycle under the existing `retention.callLogs` window, so identity nodes whose call-log content has already been purged no longer accumulate without bound in `storage.sqlite` (#12548 — thanks @pacocartones) diff --git a/src/app/api/settings/purge-usage-history/route.ts b/src/app/api/settings/purge-usage-history/route.ts index 12d1413567..7cedcf5e6c 100644 --- a/src/app/api/settings/purge-usage-history/route.ts +++ b/src/app/api/settings/purge-usage-history/route.ts @@ -54,6 +54,8 @@ export async function POST(request: Request) { deletedRoutingDecisions: result.deletedRoutingDecisions, deletedQuotaConsumption: result.deletedQuotaConsumption, deletedTokenLedger: result.deletedTokenLedger, + deletedConversationTurnNodes: result.deletedConversationTurnNodes, + deletedAgenticConversations: result.deletedAgenticConversations, errors: result.errors, }, { status: result.errors > 0 ? 500 : 200 } diff --git a/src/lib/db/cleanup.ts b/src/lib/db/cleanup.ts index a837909df7..01e84a7964 100644 --- a/src/lib/db/cleanup.ts +++ b/src/lib/db/cleanup.ts @@ -13,6 +13,7 @@ import { deleteAllFromTable, deleteCallLogArtifacts, deleteFromTableBefore, + deleteFromTableBeforeInBatches, tableExists, type DeleteByPeriodTarget, } from "./cleanup/usagePurge"; @@ -430,6 +431,103 @@ export async function cleanupCcrBlocks(): Promise { return result; } +/** + * Clean up conversation_turn_nodes older than the call-log retention window (#12453). + * + * The nodes are identity-only: the transcript view resolves each turn's display + * content from the call_logs row `last_correlation_id` points at. Once + * cleanupCallLogs purges that row the node can never render again, so the two + * tables share the dashboard database setting `retention.callLogs` instead of + * a knob of their own; `CALL_LOG_RETENTION_DAYS` configures the separate + * compliance cleanup path and does not override this window. Deleting an old + * node only affects reconnect anchors: a conversation resumed after the window + * mints a new id, which is already the documented anchor-miss behavior of + * resolveConversationId. `last_seen_at` has no index (migration 156), so + * each DELETE is a table scan. Bounded batches yield between writes so an + * existing large table cannot park the event loop for the whole cleanup pass. + */ +export async function cleanupConversationTurnNodes(): Promise { + const retention = getRetentionSettings(); + + const retentionDays = retention.callLogs; + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + const cutoffISO = cutoffDate.toISOString(); + + const result: CleanupResult = { deleted: 0, errors: 0 }; + + try { + result.deleted = await deleteFromTableBeforeInBatches( + { table: "conversation_turn_nodes", column: "last_seen_at", cutoff: "iso" }, + cutoffISO + ); + + console.log( + `[Cleanup] Deleted ${result.deleted} conversation_turn_nodes older than ${retentionDays} days` + ); + } catch (err: unknown) { + console.error("[Cleanup] Error cleaning conversation_turn_nodes:", err); + result.errors++; + } + + return result; +} + +/** + * Sweep agentic_conversations left without any conversation_turn_nodes (#12453). + * + * Runs after cleanupConversationTurnNodes so a root whose whole chain just + * expired goes in the same pass. The indexed `last_seen_at` predicate bounds + * the NOT EXISTS probe to roots that are already past the retention window. + * Deletion is batched for the same event-loop fairness guarantee as the + * preceding node cleanup. + */ +export async function cleanupAgenticConversations(): Promise { + const db = getDbInstance(); + const retention = getRetentionSettings(); + + const retentionDays = retention.callLogs; + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + const cutoffISO = cutoffDate.toISOString(); + + const result: CleanupResult = { deleted: 0, errors: 0 }; + + try { + if (!tableExists("agentic_conversations") || !tableExists("conversation_turn_nodes")) { + return result; + } + + const stmt = db.prepare( + `DELETE FROM agentic_conversations + WHERE rowid IN ( + SELECT rowid FROM agentic_conversations + WHERE last_seen_at < ? + AND NOT EXISTS ( + SELECT 1 FROM conversation_turn_nodes n + WHERE n.conversation_id = agentic_conversations.id + ) + LIMIT 10000 + )` + ); + while (true) { + const batch = stmt.run(cutoffISO).changes; + result.deleted += batch; + if (batch < 10_000) break; + await new Promise((resolve) => setImmediate(resolve)); + } + + console.log( + `[Cleanup] Deleted ${result.deleted} orphaned agentic_conversations older than ${retentionDays} days` + ); + } catch (err: unknown) { + console.error("[Cleanup] Error cleaning agentic_conversations:", err); + result.errors++; + } + + return result; +} + /** * Run all cleanup functions if auto-cleanup is enabled. */ @@ -463,6 +561,8 @@ export async function runAutoCleanup(): Promise<{ compressionRunTelemetry: await cleanupCompressionRunTelemetry(), proxyLogs: await cleanupProxyLogs(), ccrBlocks: await cleanupCcrBlocks(), + conversationTurnNodes: await cleanupConversationTurnNodes(), + agenticConversations: await cleanupAgenticConversations(), }; const totalDeleted = Object.values(results).reduce((sum, r) => sum + r.deleted, 0); @@ -588,6 +688,8 @@ export interface ResetUsageHistoryResult extends CleanupResult { deletedRoutingDecisions: number; deletedQuotaConsumption: number; deletedTokenLedger: number; + deletedConversationTurnNodes: number; + deletedAgenticConversations: number; } function isResetUsageHistoryPeriod(period: string): period is ResetUsageHistoryPeriod { @@ -604,10 +706,13 @@ function isResetUsageHistoryPeriod(period: string): period is ResetUsageHistoryP * first, since the whole point is to wipe the data the user selected. * * @param period - One of {@link RESET_USAGE_HISTORY_PERIODS}. `"all"` wipes - * every row in all three tables; any other value deletes rows strictly - * older than `now - period`. Throws on an invalid period. + * every reset target, including conversation identity metadata; any other + * value deletes only time-scoped usage/log rows older than `now - period`. + * Throws on an invalid period. */ -const RESET_TARGETS: Array = [ +const RESET_TARGETS: Array< + DeleteByPeriodTarget & { resultKey: keyof ResetUsageHistoryResult; allOnly?: boolean } +> = [ { table: "usage_history", column: "timestamp", cutoff: "iso", resultKey: "deletedUsageHistory" }, { table: "daily_usage_summary", @@ -660,6 +765,20 @@ const RESET_TARGETS: Array { @@ -684,6 +803,8 @@ export async function resetUsageHistory(period: string): Promise { - switch (target.cutoff) { - case "date": - return cutoffIso.slice(0, 10); - case "dateHour": - return `${cutoffIso.slice(0, 10)} ${cutoffIso.slice(11, 13)}:00:00`; - case "epochMs": - return new Date(cutoffIso).getTime(); - case "epochSeconds": - return Math.floor(new Date(cutoffIso).getTime() / 1000); - case "iso": - default: - return cutoffIso; - } - })(); - return getDbInstance() .prepare(`DELETE FROM ${target.table} WHERE ${target.column} < ?`) - .run(cutoff).changes; + .run(cutoffValue(target, cutoffIso)).changes; +} + +export async function deleteFromTableBeforeInBatches( + target: DeleteByPeriodTarget, + cutoffIso: string +): Promise { + if (!tableExists(target.table)) return 0; + + const statement = getDbInstance().prepare( + `DELETE FROM ${target.table} + WHERE rowid IN ( + SELECT rowid FROM ${target.table} + WHERE ${target.column} < ? + LIMIT ? + )` + ); + const cutoff = cutoffValue(target, cutoffIso); + let deleted = 0; + + while (true) { + const batch = statement.run(cutoff, DELETE_BATCH_SIZE).changes; + deleted += batch; + if (batch < DELETE_BATCH_SIZE) return deleted; + await new Promise((resolve) => setImmediate(resolve)); + } } export function collectCallLogArtifactsBefore(cutoffIso: string): string[] { diff --git a/tests/unit/db-cleanup-conversation-nodes-12453.test.ts b/tests/unit/db-cleanup-conversation-nodes-12453.test.ts new file mode 100644 index 0000000000..a285fbc1f7 --- /dev/null +++ b/tests/unit/db-cleanup-conversation-nodes-12453.test.ts @@ -0,0 +1,190 @@ +/** + * Issue #12453 — conversation_turn_nodes / agentic_conversations have no + * retention path, so storage.sqlite grows without bound (1.15M node rows, + * ~775 MB in four days on one busy coding-agent workload). + * + * The identity nodes only make sense while the call_logs row their + * last_correlation_id points at still exists, so both tables follow the + * existing `retention.callLogs` window instead of getting a knob of their own. + * + * These tests call the REAL cleanup functions against a real SQLite adapter + * seeded with test rows, exactly like telemetry-auto-cleanup-6848.test.ts. + * + * DATA_DIR isolation is self-contained (mkdtempSync below), not dependent on + * the test:unit harness's `--import ./tests/_setup/isolateDataDir.ts`: this + * file runs real DELETEs through getDbInstance(), which resolves to the + * developer's ~/.omniroute/storage.sqlite when DATA_DIR is unset. Do NOT + * remove the DATA_DIR override below. + */ + +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-12453-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const { cleanupConversationTurnNodes, cleanupAgenticConversations, runAutoCleanup } = + await import("../../src/lib/db/cleanup.ts"); +const { getDbInstance, resetDbInstance } = await import("../../src/lib/db/core.ts"); +const { getUserDatabaseSettings } = await import("../../src/lib/db/databaseSettings.ts"); + +test.after(() => { + resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +const DAY_MS = 86_400_000; +const RETENTION_DAYS = getUserDatabaseSettings().retention.callLogs; +const OLD = new Date(Date.now() - (RETENTION_DAYS + 1) * DAY_MS).toISOString(); +const RECENT = new Date().toISOString(); + +function insertConversation(id: string, lastSeenAt: string): void { + getDbInstance()! + .prepare( + `INSERT INTO agentic_conversations + (id, api_key_id, fingerprint_hash, last_message_count, last_messages_hash, turn_count, first_seen_at, last_seen_at) + VALUES (?, 'key1', 'fp', 0, '', 1, ?, ?)` + ) + .run(id, lastSeenAt, lastSeenAt); +} + +function insertNode(id: string, conversationId: string, lastSeenAt: string): void { + getDbInstance()! + .prepare( + `INSERT INTO conversation_turn_nodes + (id, conversation_id, parent_id, role, content_hash, last_correlation_id, first_seen_at, last_seen_at) + VALUES (?, ?, NULL, 'user', 'hash', 'corr', ?, ?)` + ) + .run(id, conversationId, lastSeenAt, lastSeenAt); +} + +function count(table: string): number { + const row = getDbInstance()!.prepare(`SELECT COUNT(*) AS cnt FROM ${table}`).get() as { + cnt: number; + }; + return row.cnt; +} + +function ids(table: string): string[] { + const rows = getDbInstance()!.prepare(`SELECT id FROM ${table} ORDER BY id`).all() as Array<{ + id: string; + }>; + return rows.map((r) => r.id); +} + +test.beforeEach(() => { + const db = getDbInstance()!; + db.exec("DELETE FROM conversation_turn_nodes"); + db.exec("DELETE FROM agentic_conversations"); +}); + +test("#12453 cleanupConversationTurnNodes: deletes nodes older than the call-log retention window", async () => { + insertConversation("conv_a", RECENT); + insertNode("old-1", "conv_a", OLD); + insertNode("old-2", "conv_a", OLD); + insertNode("old-3", "conv_a", OLD); + insertNode("recent-1", "conv_a", RECENT); + insertNode("recent-2", "conv_a", RECENT); + + const result = await cleanupConversationTurnNodes(); + + assert.strictEqual(result.deleted, 3); + assert.strictEqual(result.errors, 0); + assert.deepStrictEqual(ids("conversation_turn_nodes"), ["recent-1", "recent-2"]); +}); + +test("#12453 cleanupConversationTurnNodes: yields between bounded delete batches", async () => { + insertConversation("conv_bulk", OLD); + const db = getDbInstance()!; + const insert = db.prepare( + `INSERT INTO conversation_turn_nodes + (id, conversation_id, parent_id, role, content_hash, last_correlation_id, first_seen_at, last_seen_at) + VALUES (?, 'conv_bulk', NULL, 'user', 'hash', 'corr', ?, ?)` + ); + db.transaction(() => { + for (let i = 0; i < 10_001; i++) insert.run(`bulk-${i}`, OLD, OLD); + })(); + + let eventLoopTurnObserved = false; + setImmediate(() => { + eventLoopTurnObserved = true; + }); + + const result = await cleanupConversationTurnNodes(); + + assert.strictEqual(result.deleted, 10_001); + assert.strictEqual(result.errors, 0); + assert.strictEqual(count("conversation_turn_nodes"), 0); + assert.strictEqual(eventLoopTurnObserved, true, "cleanup should yield after a full batch"); +}); + +test("#12453 cleanupAgenticConversations: sweeps stale conversations that have no nodes left", async () => { + // Stale and orphaned: every node already expired -> must go. + insertConversation("conv_orphan_old", OLD); + // Stale but still anchored by a live node -> must stay. + insertConversation("conv_anchored", OLD); + insertNode("live-1", "conv_anchored", RECENT); + // Fresh root whose nodes are not written yet (createConversation runs before + // the node insert in the same request) -> must stay. + insertConversation("conv_fresh_no_nodes", RECENT); + + const result = await cleanupAgenticConversations(); + + assert.strictEqual(result.deleted, 1); + assert.strictEqual(result.errors, 0); + assert.deepStrictEqual(ids("agentic_conversations"), ["conv_anchored", "conv_fresh_no_nodes"]); + assert.strictEqual(count("conversation_turn_nodes"), 1); +}); + +test("#12453 nodes expire first, then the conversation they anchored is swept in the same pass", async () => { + insertConversation("conv_dead", OLD); + insertNode("dead-1", "conv_dead", OLD); + insertNode("dead-2", "conv_dead", OLD); + + // Conversation-only sweep must not touch a root that still has (old) nodes. + const first = await cleanupAgenticConversations(); + assert.strictEqual(first.deleted, 0); + assert.strictEqual(count("agentic_conversations"), 1); + + const nodes = await cleanupConversationTurnNodes(); + assert.strictEqual(nodes.deleted, 2); + + const second = await cleanupAgenticConversations(); + assert.strictEqual(second.deleted, 1); + assert.strictEqual(count("agentic_conversations"), 0); +}); + +test("#12453 runAutoCleanup: registers both tables and reports them in results", async () => { + insertConversation("conv_x", OLD); + insertNode("x-1", "conv_x", OLD); + insertConversation("conv_y", RECENT); + insertNode("y-1", "conv_y", RECENT); + + const summary = await runAutoCleanup(); + + assert.ok(summary.results.conversationTurnNodes, "conversationTurnNodes missing from results"); + assert.ok(summary.results.agenticConversations, "agenticConversations missing from results"); + assert.strictEqual(summary.results.conversationTurnNodes.deleted, 1); + assert.strictEqual(summary.results.agenticConversations.deleted, 1); + assert.strictEqual(summary.results.conversationTurnNodes.errors, 0); + assert.strictEqual(summary.results.agenticConversations.errors, 0); + assert.deepStrictEqual(ids("conversation_turn_nodes"), ["y-1"]); + assert.deepStrictEqual(ids("agentic_conversations"), ["conv_y"]); +}); + +test("#12453 cleanupAgenticConversations: missing node table is a safe no-op", async () => { + insertConversation("conv_without_table", OLD); + const db = getDbInstance()!; + db.exec("ALTER TABLE conversation_turn_nodes RENAME TO conversation_turn_nodes_unavailable"); + + try { + const result = await cleanupAgenticConversations(); + assert.deepStrictEqual(result, { deleted: 0, errors: 0 }); + assert.strictEqual(count("agentic_conversations"), 1); + } finally { + db.exec("ALTER TABLE conversation_turn_nodes_unavailable RENAME TO conversation_turn_nodes"); + } +}); diff --git a/tests/unit/usage-history-reset.test.ts b/tests/unit/usage-history-reset.test.ts index 01f0d86d4e..86d254193c 100644 --- a/tests/unit/usage-history-reset.test.ts +++ b/tests/unit/usage-history-reset.test.ts @@ -58,6 +58,24 @@ test.after(() => { } }); +test("purge usage API exposes every conversation reset counter", () => { + const routeSource = fs.readFileSync( + path.join(process.cwd(), "src/app/api/settings/purge-usage-history/route.ts"), + "utf8" + ); + + assert.match( + routeSource, + /deletedConversationTurnNodes:\s*result\.deletedConversationTurnNodes/, + "the API response should expose deleted conversation nodes" + ); + assert.match( + routeSource, + /deletedAgenticConversations:\s*result\.deletedAgenticConversations/, + "the API response should expose deleted conversation roots" + ); +}); + test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hourly_usage_summary; a period only deletes rows older than the cutoff; an invalid period throws", async () => { setup(); try { @@ -103,6 +121,17 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou "INSERT INTO combos (id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)" ).run("combo-test", "Test Combo", "{}", recentIso, recentIso); + db.prepare( + `INSERT INTO agentic_conversations + (id, api_key_id, fingerprint_hash, last_message_count, last_messages_hash, turn_count, first_seen_at, last_seen_at) + VALUES ('conversation-test', 'key-test', 'fp', 0, '', 1, ?, ?)` + ).run(recentIso, recentIso); + db.prepare( + `INSERT INTO conversation_turn_nodes + (id, conversation_id, parent_id, role, content_hash, last_correlation_id, first_seen_at, last_seen_at) + VALUES ('turn-test', 'conversation-test', NULL, 'user', 'hash', 'recent-call', ?, ?)` + ).run(recentIso, recentIso); + db.prepare("INSERT INTO usage_history (provider, model, timestamp) VALUES (?, ?, ?)").run( "openai", "gpt-test", @@ -240,6 +269,16 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou assert.equal(countRows(db, "provider_nodes"), 1, "provider config should survive reset"); assert.equal(countRows(db, "api_keys"), 1, "API keys should survive reset"); assert.equal(countRows(db, "combos"), 1, "combos should survive reset"); + assert.equal( + countRows(db, "conversation_turn_nodes"), + 1, + "a timed reset should preserve conversation identity nodes" + ); + assert.equal( + countRows(db, "agentic_conversations"), + 1, + "a timed reset should preserve conversation roots" + ); assert.equal(countRows(db, "usage_history"), 1, "recent usage_history row should survive"); assert.equal(countRows(db, "call_logs"), 1, "recent call_logs row should survive"); @@ -310,6 +349,16 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou 1, "'all' should delete remaining call artifact" ); + assert.equal( + allResult.deletedConversationTurnNodes, + 1, + "'all' should delete conversation identity nodes" + ); + assert.equal( + allResult.deletedAgenticConversations, + 1, + "'all' should delete conversation roots" + ); assert.equal( fs.existsSync(recentArtifactPath), false, @@ -331,6 +380,16 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou 0, "'all' should empty hourly_usage_summary" ); + assert.equal( + countRows(db, "conversation_turn_nodes"), + 0, + "'all' should empty conversation_turn_nodes" + ); + assert.equal( + countRows(db, "agentic_conversations"), + 0, + "'all' should empty agentic_conversations" + ); assert.equal(countRows(db, "provider_nodes"), 1, "provider config should still survive 'all'"); assert.equal(countRows(db, "api_keys"), 1, "API keys should still survive 'all'"); assert.equal(countRows(db, "combos"), 1, "combos should still survive 'all'"); From 00421fde0cec1ed7f43a5d330908d2131c7086ae Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:41:56 +0200 Subject: [PATCH 08/20] fix(devin): accept Windows sandbox paths in the agentic home check (#12545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DEVIN_AGENTIC_HOME` was unusable on every Windows host whatever its value, because a forward-slash `.sandbox` check can never match a backslash path. Normalizing separators before comparing, the same way `normalizeCommandToken` does, is the consistent fix; keeping `path.isAbsolute()` untouched keeps the guard intact. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12545-devin-windows-agentic-home-check.md | 1 + docs/reference/ENVIRONMENT.md | 2 +- open-sse/executors/devin-cli-agentic.ts | 10 ++++-- .../executor-devin-cli-agentic-acp.test.ts | 34 ++++++++++++++++++- 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixes/12545-devin-windows-agentic-home-check.md diff --git a/changelog.d/fixes/12545-devin-windows-agentic-home-check.md b/changelog.d/fixes/12545-devin-windows-agentic-home-check.md new file mode 100644 index 0000000000..fd59162f4d --- /dev/null +++ b/changelog.d/fixes/12545-devin-windows-agentic-home-check.md @@ -0,0 +1 @@ +- **fix(devin):** accept Windows `DEVIN_AGENTIC_HOME` sandbox paths (`C:\...\.sandbox\...`) in the isolated-home check so the Devin Claude Bridge no longer fails closed on Windows ([#12405](https://github.com/diegosouzapw/OmniRoute/issues/12405)) (#12545 — thanks @pacocartones) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 13b70dd4e3..bce25352e8 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -430,7 +430,7 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex, | `DEVIN_DESKTOP_VERSION` | `3.6.27` | `open-sse/executors/devin-desktop.ts` | Devin Desktop `ide_version`. Overrides must use `x.y.z` format; invalid values fall back to the verified default. | | `DEVIN_DESKTOP_EXTENSION_VERSION` | `1.48.2` | `open-sse/executors/devin-desktop.ts` | Bundled Codeium/language-server `extension_version`, distinct from Desktop `ide_version`. Overrides must use `x.y.z`; invalid values use the bundled default. | | `CLI_DEVIN_AGENTIC_BIN` | `devin` | `open-sse/executors/devin-cli-agentic.ts` | Agentic bridge-only Devin CLI override. The executor accepts only the local ACP stdio upstream. | -| `DEVIN_AGENTIC_HOME` | _(required)_ | `open-sse/executors/devin-cli-agentic.ts` | Absolute isolated home for the agentic Devin subprocess; accepted bridge paths are `/home/bridge` and task-local `.sandbox` paths. | +| `DEVIN_AGENTIC_HOME` | _(required)_ | `open-sse/executors/devin-cli-agentic.ts` | Absolute isolated home for the agentic Devin subprocess; accepted bridge paths are `/home/bridge` and task-local `.sandbox` paths (on Windows, `C:\...\.sandbox\...`). | | `DEVIN_AGENTIC_ACP_TIMEOUT_MS` | `120000` | `open-sse/executors/devin-cli-agentic.ts` | Maximum duration of one Devin ACP turn before the bridge terminates the child and returns an explicit timeout. | | `DEVIN_BRIDGE_MODEL` | `devin-cli-agentic/swe-1-7` | `docker/devin-bridge/compose.yml` | Main Claude Code model alias for the isolated bridge. The live harness replaces the example with a model returned by the current Devin account. | | `DEVIN_BRIDGE_SONNET_MODEL` | `DEVIN_BRIDGE_MODEL` | `docker/devin-bridge/compose.yml` | Isolated bridge alias used when Claude Code requests its Sonnet default. | diff --git a/open-sse/executors/devin-cli-agentic.ts b/open-sse/executors/devin-cli-agentic.ts index 9180a02c61..c159a65e4c 100644 --- a/open-sse/executors/devin-cli-agentic.ts +++ b/open-sse/executors/devin-cli-agentic.ts @@ -115,8 +115,12 @@ export function assertLocalAcpUrl(url: string): void { } } -function isIsolatedHome(value: string): boolean { - return value === "/home/bridge" || value.includes("/.sandbox/"); +// Accepts `/home/bridge` or any path with a `.sandbox` directory segment. Windows hosts +// hand in backslash paths (`C:\Users\...\.sandbox\home`), which used to fail closed +// unconditionally because the separator never matched (#12405). +export function isIsolatedDevinHome(value: string): boolean { + const normalized = value.replace(/\\/g, "/"); + return normalized === "/home/bridge" || normalized.includes("/.sandbox/"); } export function buildDevinChildEnv( @@ -124,7 +128,7 @@ export function buildDevinChildEnv( source: NodeJS.ProcessEnv = process.env ): NodeJS.ProcessEnv { const home = source.DEVIN_AGENTIC_HOME?.trim() || ""; - if (!home || !path.isAbsolute(home) || !isIsolatedHome(home)) { + if (!home || !path.isAbsolute(home) || !isIsolatedDevinHome(home)) { throw new DevinAgenticBridgeError( "DEVIN_AGENTIC_HOME must be an absolute path inside the bridge sandbox", "unsafe_devin_home", diff --git a/tests/unit/executor-devin-cli-agentic-acp.test.ts b/tests/unit/executor-devin-cli-agentic-acp.test.ts index 3300983904..827c6aa137 100644 --- a/tests/unit/executor-devin-cli-agentic-acp.test.ts +++ b/tests/unit/executor-devin-cli-agentic-acp.test.ts @@ -12,7 +12,7 @@ process.env.DEVIN_AGENTIC_HOME = process.env.HOME; fs.mkdirSync(process.env.HOME, { recursive: true }); fs.mkdirSync(process.env.DATA_DIR, { recursive: true }); -const { assertLocalAcpUrl, buildDevinChildEnv, DevinCliAgenticExecutor } = +const { assertLocalAcpUrl, buildDevinChildEnv, DevinCliAgenticExecutor, isIsolatedDevinHome } = await import("../../open-sse/executors/devin-cli-agentic.ts"); const { devin_cli_agenticProvider } = await import("../../open-sse/config/providers/registry/devin-cli-agentic/index.ts"); @@ -67,6 +67,38 @@ test("Devin child environment is allowlisted and requires an isolated home", () ); }); +test("Devin isolated-home check accepts Windows sandbox paths (#12405)", () => { + // CI unit tests run on Linux, where path.isAbsolute() rejects "C:\\..." before the + // sandbox check runs, so the pure helper is exercised directly with Windows strings. + for (const home of [ + "C:\\Users\\example\\.sandbox\\home", + "C:\\Users\\example\\.sandbox\\devin-sandbox\\home", + "D:/omniroute/.sandbox/home", + "\\\\server\\share\\.sandbox\\home", + "/home/bridge", + "/opt/omniroute/.sandbox/unit-home", + ]) { + assert.equal(isIsolatedDevinHome(home), true, `accepts ${home}`); + } + for (const home of [ + "C:\\Users\\example", + "C:\\Users\\example\\devin-sandbox", + "C:\\Users\\example\\.sandbox", + "C:\\Users\\example\\sandbox\\home", + "/tmp/outside", + "/home/bridge2", + "", + ]) { + assert.equal(isIsolatedDevinHome(home), false, `rejects ${home}`); + } + // Absoluteness is still enforced by the caller, not by the sandbox-segment helper. + assert.throws( + () => + buildDevinChildEnv({}, { PATH: "/usr/bin", DEVIN_AGENTIC_HOME: "relative/.sandbox/home" }), + /inside the bridge sandbox/ + ); +}); + test("Devin child environment derives only the trusted bridge proxy", () => { const isolatedHome = path.join(process.cwd(), ".sandbox", "unit-home"); const trustedProxy = "http://network-guard:8080"; From a606df0b5d6c1a9e407b65c31a5f044f5311b5c0 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:00 +0200 Subject: [PATCH 09/20] fix(video): clamp estimateJpegFrameBytes, reuse the shared JPEG prefix (#12543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A documented byte estimate should never come back negative, even where today's usage makes it harmless — `Math.max(0, …)` is the honest floor. Replacing the three hardcoded `data:image/jpeg;base64,` literals with the shared constant is byte-identical and removes three chances to drift. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../fixes/12543-video-frame-estimate-clamp.md | 1 + src/lib/guardrails/videoBridgeContactSheet.ts | 8 +++- .../videoBridgeDrilldownLifecycle.ts | 3 +- .../guardrails/videoBridgeFrameContract.ts | 3 +- src/lib/guardrails/videoBridgeRuntime.ts | 4 +- .../videoBridgeFrameContract.test.ts | 38 +++++++++++++++++++ 6 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixes/12543-video-frame-estimate-clamp.md diff --git a/changelog.d/fixes/12543-video-frame-estimate-clamp.md b/changelog.d/fixes/12543-video-frame-estimate-clamp.md new file mode 100644 index 0000000000..767c8fb587 --- /dev/null +++ b/changelog.d/fixes/12543-video-frame-estimate-clamp.md @@ -0,0 +1 @@ +- **fix(video):** Clamp `estimateJpegFrameBytes` at zero for padding-only payloads and build the three encode-side frame data URIs from `JPEG_FRAME_DATA_URI_PREFIX` instead of a repeated literal (#12543 — thanks @pacocartones) diff --git a/src/lib/guardrails/videoBridgeContactSheet.ts b/src/lib/guardrails/videoBridgeContactSheet.ts index 4fdf18e258..69b76e932b 100644 --- a/src/lib/guardrails/videoBridgeContactSheet.ts +++ b/src/lib/guardrails/videoBridgeContactSheet.ts @@ -1,4 +1,8 @@ -import { decodeJpegFrameDataUri, estimateJpegFrameBytes } from "./videoBridgeFrameContract"; +import { + JPEG_FRAME_DATA_URI_PREFIX, + decodeJpegFrameDataUri, + estimateJpegFrameBytes, +} from "./videoBridgeFrameContract"; import { VIDEO_FRAME_MAX_BYTES } from "./videoBridgeRuntime"; export interface ContactSheetFrame { @@ -120,7 +124,7 @@ export async function buildVideoContactSheet( if (signal.aborted) throw new Error("Video contact sheet was aborted"); if (output.byteLength > MAX_SHEET_BYTES) return fallback(frames); return { - dataUri: `data:image/jpeg;base64,${output.toString("base64")}`, + dataUri: `${JPEG_FRAME_DATA_URI_PREFIX}${output.toString("base64")}`, frames: frames.map((frame) => ({ ...frame })), height: rows * TILE_SIZE, timestamps: frames.map((frame) => frame.timestampSeconds), diff --git a/src/lib/guardrails/videoBridgeDrilldownLifecycle.ts b/src/lib/guardrails/videoBridgeDrilldownLifecycle.ts index 94728e9b98..5b70f0d6d3 100644 --- a/src/lib/guardrails/videoBridgeDrilldownLifecycle.ts +++ b/src/lib/guardrails/videoBridgeDrilldownLifecycle.ts @@ -22,6 +22,7 @@ import { type VideoDrilldownPutValue, type VideoDrilldownResult, } from "./videoBridgeDrilldown"; +import { JPEG_FRAME_DATA_URI_PREFIX } from "./videoBridgeFrameContract"; export type VideoDrilldownVariant = "preview" | "standard" | "detail"; @@ -170,7 +171,7 @@ async function shrinkFrameForVariant( .toBuffer(); const metadata = await sharp(resized).metadata(); return { - dataUri: `data:image/jpeg;base64,${resized.toString("base64")}`, + dataUri: `${JPEG_FRAME_DATA_URI_PREFIX}${resized.toString("base64")}`, height: metadata.height ?? frame.height, timestampSeconds: frame.timestampSeconds, width: metadata.width ?? frame.width, diff --git a/src/lib/guardrails/videoBridgeFrameContract.ts b/src/lib/guardrails/videoBridgeFrameContract.ts index 996c3c5ea3..a4c2af69e3 100644 --- a/src/lib/guardrails/videoBridgeFrameContract.ts +++ b/src/lib/guardrails/videoBridgeFrameContract.ts @@ -24,5 +24,6 @@ export function decodeJpegFrameDataUri(dataUri: string): Buffer { export function estimateJpegFrameBytes(dataUri: string): number { const encoded = matchJpegFrame(dataUri); const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0; - return Math.floor((encoded.length * 3) / 4) - padding; + // Padding-only payloads (e.g. "=") pass the charset pattern; never report a negative size. + return Math.max(0, Math.floor((encoded.length * 3) / 4) - padding); } diff --git a/src/lib/guardrails/videoBridgeRuntime.ts b/src/lib/guardrails/videoBridgeRuntime.ts index 7099acd231..36954ed5ea 100644 --- a/src/lib/guardrails/videoBridgeRuntime.ts +++ b/src/lib/guardrails/videoBridgeRuntime.ts @@ -4,6 +4,8 @@ import { tmpdir } from "node:os"; import { isAbsolute, join } from "node:path"; import { promisify } from "node:util"; +import { JPEG_FRAME_DATA_URI_PREFIX } from "./videoBridgeFrameContract"; + const execFileAsync = promisify(execFile); export interface VideoCommandOptions { @@ -997,7 +999,7 @@ export async function extractVideoFramesFromBytes( return { durationSeconds: metadata.durationSeconds, frames: frameFiles.map((frame, index) => ({ - dataUri: `data:image/jpeg;base64,${frameBytes[index].toString("base64")}`, + dataUri: `${JPEG_FRAME_DATA_URI_PREFIX}${frameBytes[index].toString("base64")}`, timestampSeconds: frame.timestampSeconds, })), sampling: frameFiles.sampling, diff --git a/tests/unit/guardrails/videoBridgeFrameContract.test.ts b/tests/unit/guardrails/videoBridgeFrameContract.test.ts index 4391b2e98a..a4751c40e3 100644 --- a/tests/unit/guardrails/videoBridgeFrameContract.test.ts +++ b/tests/unit/guardrails/videoBridgeFrameContract.test.ts @@ -1,5 +1,8 @@ import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; import test from "node:test"; +import { fileURLToPath } from "node:url"; import { JPEG_FRAME_DATA_URI_PREFIX, @@ -33,3 +36,38 @@ test("estimates decoded bytes without decoding, accounting for padding", () => { assert.equal(estimateJpegFrameBytes(uri), Buffer.byteLength(source)); } }); + +test("never estimates below zero for degenerate padding-only payloads (#12323)", () => { + // The charset-only pattern admits these; the estimate must clamp instead of going to -1. + for (const encoded of ["=", "==", "A=", "A=="]) { + const uri = `${JPEG_FRAME_DATA_URI_PREFIX}${encoded}`; + const estimate = estimateJpegFrameBytes(uri); + assert.ok(estimate >= 0, `${JSON.stringify(encoded)} estimated ${estimate}`); + assert.ok( + estimate >= decodeJpegFrameDataUri(uri).byteLength, + `${JSON.stringify(encoded)} estimate is not an upper bound` + ); + } + assert.equal(estimateJpegFrameBytes(`${JPEG_FRAME_DATA_URI_PREFIX}=`), 0); + assert.equal(estimateJpegFrameBytes(`${JPEG_FRAME_DATA_URI_PREFIX}==`), 0); +}); + +test("encode sites build frame data URIs from JPEG_FRAME_DATA_URI_PREFIX (#12323)", () => { + const guardrailsDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../../src/lib/guardrails" + ); + for (const file of [ + "videoBridgeContactSheet.ts", + "videoBridgeRuntime.ts", + "videoBridgeDrilldownLifecycle.ts", + ]) { + const source = fs.readFileSync(path.join(guardrailsDir, file), "utf8"); + assert.doesNotMatch(source, /data:image\/jpeg;base64,/, `${file} hardcodes the JPEG prefix`); + assert.match( + source, + /\bJPEG_FRAME_DATA_URI_PREFIX\b/, + `${file} does not use the shared prefix` + ); + } +}); From 7ad5e1120eaf786377d85138f5555f44453fb789 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:05 +0200 Subject: [PATCH 10/20] fix(api-manager): accessible loading status on the skeleton gate (#12541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A real accessibility hole, not a cosmetic one: for the whole loading window the page exposed nothing but the sidebar, which is exactly the "API Keys link does nothing" report. Reusing the `role="status" aria-live="polite"` container the other dashboard loading states already use keeps it consistent. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12541-api-manager-skeleton-a11y-status.md | 1 + .../api-manager/ApiManagerPageClient.tsx | 5 +- .../api-manager-loading-status-12066.test.tsx | 76 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/12541-api-manager-skeleton-a11y-status.md create mode 100644 tests/unit/ui/api-manager-loading-status-12066.test.tsx diff --git a/changelog.d/fixes/12541-api-manager-skeleton-a11y-status.md b/changelog.d/fixes/12541-api-manager-skeleton-a11y-status.md new file mode 100644 index 0000000000..2896aef4da --- /dev/null +++ b/changelog.d/fixes/12541-api-manager-skeleton-a11y-status.md @@ -0,0 +1 @@ +- **fix(api-manager):** Expose an accessible loading status while API keys are fetched instead of an empty accessibility tree (#12541 — thanks @pacocartones) diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index aa5b997aa9..592894cafb 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -948,8 +948,11 @@ export default function ApiManagerPageClient() { }, [modelsByProvider, debouncedSearchModel]); if (loading) { + // The skeleton cards are aria-hidden, so without this status wrapper the page + // has no accessible content at all until /api/keys settles (#12066). return ( -
+
+ {tc("loading")}
diff --git a/tests/unit/ui/api-manager-loading-status-12066.test.tsx b/tests/unit/ui/api-manager-loading-status-12066.test.tsx new file mode 100644 index 0000000000..48e5e33901 --- /dev/null +++ b/tests/unit/ui/api-manager-loading-status-12066.test.tsx @@ -0,0 +1,76 @@ +// @vitest-environment jsdom + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } +).IS_REACT_ACT_ENVIRONMENT = true; + +const translate = (key: string) => key; +vi.mock("next-intl", () => ({ + useLocale: () => "en", + useTranslations: () => Object.assign(translate, { has: () => false, rich: translate }), +})); + +const { default: ApiManagerPageClient } = + await import("@/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient"); + +const roots: Array<{ root: ReturnType; container: HTMLDivElement }> = []; + +function mountPage() { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + roots.push({ root, container }); + act(() => root.render()); + return container; +} + +afterEach(() => { + for (const { root, container } of roots.splice(0)) { + act(() => root.unmount()); + container.remove(); + } + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("API manager loading gate accessibility (#12066)", () => { + it("exposes a busy polite status while the initial /api/keys fetch is pending", () => { + // Never settles: the page stays on its skeleton gate for the whole test. + vi.stubGlobal( + "fetch", + vi.fn(() => new Promise(() => undefined)) + ); + + const container = mountPage(); + const status = container.querySelector('[role="status"]'); + + expect(status).not.toBeNull(); + expect(status?.getAttribute("aria-live")).toBe("polite"); + expect(status?.getAttribute("aria-busy")).toBe("true"); + // The only text in the accessibility tree during the gate is the loading label. + expect(status?.textContent).toContain("loading"); + // The skeleton cards themselves stay decorative. + expect(container.querySelectorAll('[aria-hidden="true"]').length).toBeGreaterThan(0); + }); + + it("drops the loading status once /api/keys has settled", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ ok: true, json: async () => ({}) })) + ); + + const container = mountPage(); + for (let i = 0; i < 40 && container.querySelector('[role="status"]'); i++) { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + }); + } + + expect(container.querySelector('[role="status"]')).toBeNull(); + expect(container.querySelector("h1")).not.toBeNull(); + }); +}); From b23b0ca68ec22e7f813fa92740e30eee199fa0d9 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:08 +0200 Subject: [PATCH 11/20] fix(gemini): strip prefixItems from Gemini tool schemas (#12540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit High-impact and precisely diagnosed: `prefixItems` missing from the strip-list rejected every tool-bearing Claude Code request routed to Gemini before generation, because Claude Code's built-in tools describe line ranges as tuples. All three tool shapes going through the same cleaner is what makes the one-key fix sufficient. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12540-gemini-strip-prefixitems-nested.md | 1 + open-sse/translator/helpers/geminiHelper.ts | 6 + tests/unit/12509-gemini-prefixitems.test.ts | 141 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 changelog.d/fixes/12540-gemini-strip-prefixitems-nested.md create mode 100644 tests/unit/12509-gemini-prefixitems.test.ts diff --git a/changelog.d/fixes/12540-gemini-strip-prefixitems-nested.md b/changelog.d/fixes/12540-gemini-strip-prefixitems-nested.md new file mode 100644 index 0000000000..d1b24b21ab --- /dev/null +++ b/changelog.d/fixes/12540-gemini-strip-prefixitems-nested.md @@ -0,0 +1 @@ +- **fix(gemini):** strip the JSON-Schema-2020-12 `prefixItems` keyword from Gemini tool schemas at every nesting level, so Claude Code tool definitions no longer fail with `400 Unknown name "prefixItems"` on Gemini models (#12540 — thanks @pacocartones) diff --git a/open-sse/translator/helpers/geminiHelper.ts b/open-sse/translator/helpers/geminiHelper.ts index 95fea6dcea..fefa882c35 100644 --- a/open-sse/translator/helpers/geminiHelper.ts +++ b/open-sse/translator/helpers/geminiHelper.ts @@ -63,6 +63,12 @@ export const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([ // it, rejecting the whole request with "Unknown name \"uniqueItems\"". // Upstream 9router already strips it alongside `contains` for the same error. "uniqueItems", + // #12509: JSON-Schema-2020-12 tuple keyword. Claude Code's built-in tools + // describe `[start_line, end_line]` ranges with it (nested under `items`), + // and Gemini's schema parser rejects the whole tool list with + // "Unknown name \"prefixItems\" ... Cannot find field". ensureArrayItems + // below still guarantees an `items` schema for the tuple-typed array. + "prefixItems", // Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf) "anyOf", "oneOf", diff --git a/tests/unit/12509-gemini-prefixitems.test.ts b/tests/unit/12509-gemini-prefixitems.test.ts new file mode 100644 index 0000000000..43ea7ab015 --- /dev/null +++ b/tests/unit/12509-gemini-prefixitems.test.ts @@ -0,0 +1,141 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { buildGeminiTools } from "../../open-sse/translator/helpers/geminiToolsSanitizer.ts"; +import { GEMINI_UNSUPPORTED_SCHEMA_KEYS } from "../../open-sse/translator/helpers/geminiHelper.ts"; + +// Issue #12509: Gemini rejects the JSON-Schema-2020-12 tuple keyword `prefixItems` in +// function_declarations parameter schemas with HTTP 400 +// `Unknown name "prefixItems" at 'tools[0].function_declarations[1].parameters.properties[5] +// .value.properties[0].value.items': Cannot find field.` — the same class of error already +// fixed for `uniqueItems` (#9617), `multipleOf`, `strict` and `encrypted` in +// GEMINI_UNSUPPORTED_SCHEMA_KEYS (open-sse/translator/helpers/geminiHelper.ts). + +type GeminiFunctionDeclaration = { name: string; parameters: Record }; + +function declarationsOf(tools: unknown[]): GeminiFunctionDeclaration[] { + const geminiTools = buildGeminiTools(tools) as Array<{ + functionDeclarations?: GeminiFunctionDeclaration[]; + }> | null; + assert.ok(geminiTools, "expected buildGeminiTools to return a tools array"); + return geminiTools.flatMap((tool) => tool.functionDeclarations ?? []); +} + +function assertNoPrefixItems(tools: unknown[]): GeminiFunctionDeclaration[] { + const declarations = declarationsOf(tools); + const serialized = JSON.stringify(declarations); + assert.equal( + serialized.includes("prefixItems"), + false, + `prefixItems leaked into the Gemini payload (would trigger upstream 400 "Unknown name \\"prefixItems\\""): ${serialized}` + ); + return declarations; +} + +// The reporter's shape: a tuple nested under `items` — an array of `[start_line, end_line]` +// ranges, i.e. `properties.ranges.items.prefixItems`. +const nestedTupleParameters = { + type: "object", + properties: { + file_path: { type: "string" }, + ranges: { + type: "array", + description: "Line ranges to read", + items: { + type: "array", + prefixItems: [{ type: "integer" }, { type: "integer" }], + items: false, + minItems: 2, + maxItems: 2, + }, + }, + }, + required: ["file_path", "ranges"], +}; + +test("buildGeminiTools strips prefixItems nested under items (OpenAI tool shape, issue #12509)", () => { + const [declaration] = assertNoPrefixItems([ + { + type: "function", + function: { + name: "read_ranges", + description: "tuple-typed array parameter nested under items", + parameters: nestedTupleParameters, + }, + }, + ]); + + const ranges = (declaration.parameters.properties as Record>) + .ranges; + assert.equal(ranges.type, "array"); + const inner = ranges.items as Record; + assert.equal(inner.type, "array"); + assert.ok(inner.items && typeof inner.items === "object", "inner array keeps an items schema"); +}); + +test("buildGeminiTools strips prefixItems from a Claude input_schema (issue #12509)", () => { + const [declaration] = assertNoPrefixItems([ + { + name: "read_ranges", + description: "Claude Messages tool shape", + input_schema: nestedTupleParameters, + }, + ]); + assert.equal(declaration.name, "read_ranges"); +}); + +test("buildGeminiTools strips a top-level prefixItems tuple and keeps a usable items schema (issue #12509)", () => { + const [declaration] = assertNoPrefixItems([ + { + type: "function", + function: { + name: "read_range", + description: "single [start_line, end_line] tuple", + parameters: { + type: "object", + properties: { + range: { + type: "array", + prefixItems: [{ type: "integer" }, { type: "integer" }], + }, + }, + required: ["range"], + }, + }, + }, + ]); + + const range = (declaration.parameters.properties as Record>) + .range; + assert.equal(range.type, "array"); + assert.ok(range.items && typeof range.items === "object", "Gemini requires items on arrays"); +}); + +test("buildGeminiTools strips prefixItems that sits next to a regular items schema (issue #12509)", () => { + const [declaration] = assertNoPrefixItems([ + { + type: "function", + function: { + name: "pair", + description: "tuple keyword as a sibling of a regular items schema", + parameters: { + type: "object", + properties: { + pair: { + type: "array", + prefixItems: [{ type: "string" }], + items: { type: "string" }, + }, + }, + }, + }, + }, + ]); + + const pair = (declaration.parameters.properties as Record>).pair; + assert.deepEqual(pair.items, { type: "string" }); +}); + +test("prefixItems is registered in GEMINI_UNSUPPORTED_SCHEMA_KEYS (issue #12509)", () => { + assert.ok(GEMINI_UNSUPPORTED_SCHEMA_KEYS.has("prefixItems")); +}); From 0831d487c0e5f84d8964c2bcf219c27ca5f461dd Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:12 +0200 Subject: [PATCH 12/20] fix(audio): resolve combo names on /v1/audio/translations (#12536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translations was the one audio route the combo-resolution fixes never reached, so the same combo name worked on `/v1/audio/transcriptions` and failed here. Following the #9382 shape rather than inventing a new one is the right call. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- ...536-audio-translations-combo-resolution.md | 1 + src/app/api/v1/audio/translations/route.ts | 122 ++++++++++++---- ...udio-translations-combo-resolution.test.ts | 131 ++++++++++++++++++ 3 files changed, 226 insertions(+), 28 deletions(-) create mode 100644 changelog.d/fixes/12536-audio-translations-combo-resolution.md create mode 100644 tests/unit/audio-translations-combo-resolution.test.ts diff --git a/changelog.d/fixes/12536-audio-translations-combo-resolution.md b/changelog.d/fixes/12536-audio-translations-combo-resolution.md new file mode 100644 index 0000000000..0f16b8f32e --- /dev/null +++ b/changelog.d/fixes/12536-audio-translations-combo-resolution.md @@ -0,0 +1 @@ +- **fix(audio):** `/v1/audio/translations` now resolves combo names the way `/v1/audio/transcriptions` already does, so a combo that `GET /v1/models` advertises is fanned out to its targets instead of being rejected with `400 Invalid translation model: . Use format: provider/model`; literal `provider/model` ids and unknown bare names behave as before (#12536 — thanks @pacocartones) diff --git a/src/app/api/v1/audio/translations/route.ts b/src/app/api/v1/audio/translations/route.ts index f0c0acfa4e..65c45d0268 100644 --- a/src/app/api/v1/audio/translations/route.ts +++ b/src/app/api/v1/audio/translations/route.ts @@ -19,6 +19,25 @@ import { } from "@/app/api/v1/_shared/rateLimit"; import { attachOmniRouteMetaToResponse } from "@/domain/omnirouteResponseMeta"; import { generateRequestId } from "@/shared/utils/requestId"; +import { getComboByName, getCombos } from "@/lib/db/combos"; +import { getDatabaseSettings } from "@/lib/db/databaseSettings"; +import { handleComboChat } from "@omniroute/open-sse/services/combo.ts"; +import { log } from "@omniroute/open-sse/utils/logger.ts"; + +/** + * Copy a multipart body, swapping only the `model` field. Combo fan-out needs one + * body per target, and the uploaded file part is reused as-is (a Blob can be read + * more than once). + */ +function withModel(formData: FormData, modelStr: string): FormData { + const next = new FormData(); + for (const [key, value] of formData.entries()) { + if (key === "model") continue; + next.append(key, value as string | Blob); + } + next.set("model", modelStr); + return next; +} /** * Handle CORS preflight @@ -33,30 +52,14 @@ export async function OPTIONS() { } /** - * POST /v1/audio/translations — translate audio to English text - * OpenAI Whisper API compatible (multipart/form-data). Unlike - * /v1/audio/transcriptions, output is always English regardless of the - * source audio language. + * Translate with one concrete `provider/model` string. Split out of POST so combo + * fan-out can invoke it once per target. */ -export async function POST(request) { - let formData; - try { - formData = await request.formData(); - } catch { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid multipart form data"); - } - - const startTime = Date.now(); - - const model = formData.get("model"); - if (!model) { - return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model"); - } - - // Enforce API key policies (model restrictions + budget limits) - const policy = await enforceApiKeyPolicy(request, model as string); - if (policy.rejection) return policy.rejection; - +async function translateWithModel( + formData: FormData, + modelStr: string, + startTime: number +): Promise { // Translation is served by the transcription-capable nodes (Whisper-style // endpoints expose both), plus general chat/responses gateways. Remote hosts are // opt-in (default OFF). @@ -65,14 +68,11 @@ export async function POST(request) { "audio-transcriptions" ); - const { provider, model: resolvedModel } = parseTranslationModel( - model as string, - dynamicProviders - ); + const { provider, model: resolvedModel } = parseTranslationModel(modelStr, dynamicProviders); if (!provider) { return errorResponse( HTTP_STATUS.BAD_REQUEST, - `Invalid translation model: ${model}. Use format: provider/model` + `Invalid translation model: ${modelStr}. Use format: provider/model` ); } @@ -84,6 +84,8 @@ export async function POST(request) { let credentials = null; if (providerConfig && providerConfig.authType !== "none") { const credentialKey = providerConfig.credentialProviderId || provider; + // NOTE: the 2nd arg of this helper is `excludeConnectionId`, not "use this + // connection" — a combo target's connectionId must never be passed here. credentials = await getProviderCredentialsWithQuotaPreflight(credentialKey); if (!credentials) { return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${provider}`); @@ -113,3 +115,67 @@ export async function POST(request) { } return response; } + +/** + * POST /v1/audio/translations — translate audio to English text + * OpenAI Whisper API compatible (multipart/form-data). Unlike + * /v1/audio/transcriptions, output is always English regardless of the + * source audio language. + */ +export async function POST(request) { + let formData; + try { + formData = await request.formData(); + } catch { + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid multipart form data"); + } + + const startTime = Date.now(); + + const model = formData.get("model"); + if (!model) { + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model"); + } + const modelStr = String(model); + + // Enforce API key policies (model restrictions + budget limits) + const policy = await enforceApiKeyPolicy(request, modelStr); + if (policy.rejection) return policy.rejection; + + // A bare name (no "/") may be a combo. /v1/models advertises combos, and chat, + // embeddings and the sibling /v1/audio/transcriptions all resolve them — + // resolving here too keeps the catalog honest and frees callers from hardcoding + // a provider's internal model id. + if (!modelStr.includes("/")) { + try { + const combo = await getComboByName(modelStr); + if (combo) { + let allCombos: Awaited> = []; + try { + allCombos = await getCombos(); + } catch {} + let settings = {}; + try { + settings = getDatabaseSettings(); + } catch {} + + return handleComboChat({ + body: { model: modelStr } as any, + combo: combo as any, + handleSingleModel: async (_reqBody: any, targetModelStr: string) => + translateWithModel(withModel(formData, targetModelStr), targetModelStr, startTime), + isModelAvailable: undefined, + log, + settings, + allCombos: allCombos as any, + relayOptions: undefined, + signal: undefined, + } as any); + } + } catch (err) { + log.error("AUDIO", `Combo resolution failed for ${modelStr}: ${err}`); + } + } + + return translateWithModel(formData, modelStr, startTime); +} diff --git a/tests/unit/audio-translations-combo-resolution.test.ts b/tests/unit/audio-translations-combo-resolution.test.ts new file mode 100644 index 0000000000..aa4defe26a --- /dev/null +++ b/tests/unit/audio-translations-combo-resolution.test.ts @@ -0,0 +1,131 @@ +// Regression test: /v1/audio/translations must resolve combo names. +// +// /v1/models advertises combos, and /v1/chat/completions, /v1/embeddings, +// /v1/audio/transcriptions (#9134), /v1/audio/speech and /v1/videos/generations +// (#10469) all resolve them — but the translation route still treated the model +// string as a literal `provider/model` id only. A combo name therefore came back as +// `400 Invalid translation model: . Use format: provider/model`, so any +// client populating a model picker from /v1/models offered an option the endpoint +// rejected, and callers had to hardcode the provider's internal model id. +// +// This asserts the combo is expanded to its target before dispatch (observed at the +// upstream fetch: URL and multipart `model`), that a literal provider/model id still +// dispatches directly, and that an unknown bare name keeps the format hint. + +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-audio-translations-combo-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const { createCombo } = await import("../../src/lib/db/combos.ts"); +const { createProviderNode } = await import("../../src/lib/db/providers.ts"); +const route = await import("../../src/app/api/v1/audio/translations/route.ts"); + +const originalFetch = globalThis.fetch; + +test.after(() => { + globalThis.fetch = originalFetch; + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +/** Minimal but structurally valid WAV so nothing rejects the upload shape. */ +function makeWav(): Blob { + const dataLen = 1600; + const b = Buffer.alloc(44 + dataLen); + b.write("RIFF", 0, "ascii"); + b.writeUInt32LE(36 + dataLen, 4); + b.write("WAVE", 8, "ascii"); + b.write("fmt ", 12, "ascii"); + b.writeUInt32LE(16, 16); + b.writeUInt16LE(1, 20); + b.writeUInt16LE(1, 22); + b.writeUInt32LE(16000, 24); + b.writeUInt32LE(32000, 28); + b.writeUInt16LE(2, 32); + b.writeUInt16LE(16, 34); + b.write("data", 36, "ascii"); + b.writeUInt32LE(dataLen, 40); + return new Blob([b], { type: "audio/wav" }); +} + +function translationRequest(model: string) { + const fd = new FormData(); + fd.set("model", model); + fd.set("file", makeWav(), "t.wav"); + return new Request("http://localhost/v1/audio/translations", { method: "POST", body: fd }); +} + +/** Capture every upstream call: URL plus the decoded multipart body the handler built. */ +function captureUpstream(): Array<{ url: string; body: string }> { + const calls: Array<{ url: string; body: string }> = []; + globalThis.fetch = (async (url: RequestInfo | URL, init: RequestInit = {}) => { + calls.push({ + url: String(url), + body: new TextDecoder().decode(init.body as Uint8Array), + }); + return new Response(JSON.stringify({ text: "ok" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) as typeof fetch; + return calls; +} + +test.before(async () => { + await createProviderNode({ + id: "openai-compatible-audio-translations-test", + type: "openai-compatible", + name: "Local STT", + prefix: "localstt", + apiType: "audio-transcriptions", + baseUrl: "http://localhost:9000/v1", + } as Parameters[0]); + + await createCombo({ + name: "traducao", + strategy: "priority", + models: [{ provider: "localstt", model: "whisper-1" }], + } as Parameters[0]); +}); + +test("a combo name is expanded to its target instead of being rejected", async () => { + const calls = captureUpstream(); + + const res = await route.POST(translationRequest("traducao")); + const body = await res.text(); + + assert.equal(res.status, 200, `combo name must not be rejected — got: ${body}`); + assert.deepEqual(JSON.parse(body), { text: "ok" }); + assert.equal(calls.length, 1, `expected exactly one upstream call, got ${calls.length}`); + assert.equal(calls[0].url, "http://localhost:9000/v1/audio/translations"); + assert.match(calls[0].body, /name="model"\r\n\r\nwhisper-1\r\n/); + assert.doesNotMatch(calls[0].body, /name="model"\r\n\r\ntraducao\r\n/); +}); + +test("a literal provider/model id still dispatches directly", async () => { + const calls = captureUpstream(); + + const res = await route.POST(translationRequest("localstt/whisper-1")); + + assert.equal(res.status, 200); + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "http://localhost:9000/v1/audio/translations"); + assert.match(calls[0].body, /name="model"\r\n\r\nwhisper-1\r\n/); +}); + +test("an unknown bare name is still rejected with the format hint", async () => { + const calls = captureUpstream(); + + const res = await route.POST(translationRequest("definitely-not-a-combo-or-model")); + const body = await res.text(); + + assert.equal(res.status, 400); + assert.match(body, /Invalid translation model/); + assert.equal(calls.length, 0); +}); From e09cb5a76885d9198faf8ed34f6c757c4cc428c7 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:16 +0200 Subject: [PATCH 13/20] chore(lifecycle): gate DEFAULT_DEGRADATION_MAP against retired ids (#12535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third hand-maintained table naming model ids and the only one outside the retired-model gate — extending `check-model-lifecycle.mjs` to cover it is the durable fix, and the three retired rows it flushed out were already dead code behind the 410 `model_shutdown` answer. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12535-lifecycle-gate-degradation-map.md | 7 +++ docs/architecture/QUALITY_GATES.md | 56 +++++++++---------- open-sse/services/backgroundTaskDetector.ts | 8 ++- scripts/check/check-model-lifecycle.mjs | 56 +++++++++++++++---- tests/unit/check-model-lifecycle-gate.test.ts | 32 ++++++++++- .../model-lifecycle-degradation-map.test.ts | 56 +++++++++++++++++++ 6 files changed, 173 insertions(+), 42 deletions(-) create mode 100644 changelog.d/maintenance/12535-lifecycle-gate-degradation-map.md create mode 100644 tests/unit/model-lifecycle-degradation-map.test.ts diff --git a/changelog.d/maintenance/12535-lifecycle-gate-degradation-map.md b/changelog.d/maintenance/12535-lifecycle-gate-degradation-map.md new file mode 100644 index 0000000000..f667999ac9 --- /dev/null +++ b/changelog.d/maintenance/12535-lifecycle-gate-degradation-map.md @@ -0,0 +1,7 @@ +- **chore(lifecycle):** `check:model-lifecycle` now also diffs `DEFAULT_DEGRADATION_MAP` + (the background-task redirect table) against the vendor lifecycle snapshot, refusing a + retired id as source or target, with a table-driven unit test beside it. Three rows + whose source the vendor had retired — `claude-sonnet-4-20250514`, `gemini-3-pro-preview` + and `gpt-5.1-codex` (whose target `gpt-5.1-codex-mini` is retired too) — were dead code, + since `checkLifecycle` answers 410 before the redirect runs; they are dropped + (#12535 — thanks @pacocartones) diff --git a/docs/architecture/QUALITY_GATES.md b/docs/architecture/QUALITY_GATES.md index 4b4315cc4d..feb11fad25 100644 --- a/docs/architecture/QUALITY_GATES.md +++ b/docs/architecture/QUALITY_GATES.md @@ -57,34 +57,34 @@ assertion weakening and other masking remain owned by the independently blocking Runs on every PR to `main`. Blocks merge on failure. -| Script (`npm run ...`) | Validates | Blocking | -| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | -| `check:node-runtime` | Node.js version is within the supported range | Yes | -| `check:cycles` | Circular imports — all `src/` + `open-sse/` modules | Yes | -| `check:route-validation:t06` | Zod schemas present on all routes (Tier 6 policy) | Yes | -| `check:any-budget:t11` | `@ts-expect-error // any` count does not exceed budget (Tier 11 catraca) | Yes | -| `check:provider-consistency` | Every provider in `providers.ts` has a matching entry in `providerRegistry.ts` (and vice-versa, within the allowlist) | Yes | -| `check:model-lifecycle` | The two hand-maintained routing tables do not point at retired models (#11503): `FITNESS_TABLE` (`taskFitness.ts`) scores no routable retired id, every `BUILT_IN_ALIASES` target is a live catalog model, and every retired id the catalog still routes is either forwarded or listed in `allowedRetiredInCatalog`. Offline — compares against the vendor snapshot `config/quality/model-lifecycle.json`, refreshed by hand with `npm run quality:refresh-model-lifecycle` (network; not wired into CI). `allowedRetiredInCatalog` is a burn-down ratchet: add an entry only with a tracking issue. | Yes | -| `check:fetch-targets` | Every `fetch("/api/...")` in client-side `src/` resolves to a real `route.ts` | Yes | -| `check:deps` | All `npm install`-able deps across every `package.json` in the repo are in `dependency-allowlist.json`; new unpinned or slopsquatted packages flagged | Yes | -| `audit:deps` | `npm audit` (root + electron) — no high/critical advisories (overlaps osv `check:vuln-ratchet`; see Rationalization Backlog) | Yes | -| `check:lockfile` | `package-lock.json` integrity — https registry, integrity hashes, no host overrides | Yes | -| `check:licenses` | SPDX license allowlist for production dependencies | Yes | -| `check:tracked-artifacts` | No build artifacts / committed `node_modules` symlinks (also runs in husky pre-commit; pre-push is intentionally light — #6716) | Yes | -| `check:file-size` | No source file exceeds the per-extension cap (ratchet: frozen large files in `frozen` list) | Yes | -| `check:error-helper` | Error responses in executors/handlers use `buildErrorBody()` / `sanitizeErrorMessage()` (Hard Rule #12) | Yes | -| `check:migration-numbering` | Migration SQL files are sequentially numbered, no gaps or duplicates | Yes | -| `check:public-creds` | No literal OAuth `client_id`/`client_secret` or Firebase Web keys outside `publicCreds.ts` (Hard Rule #11) | Yes | -| `check:db-rules` | No raw SQL outside `src/lib/db/` modules; no barrel-imports from `localDb.ts` (Hard Rules #2/#5) | Yes | -| `check:known-symbols` | Provider executors, routing strategies, and translators registered in their dispatch tables match the files on disk — no orphaned or undeclared symbols | Yes | -| `check:route-guard-membership` | Every route that spawns a child process is classified by `isLocalOnlyPath()` (Hard Rules #15/#17) | Yes | -| `check:test-discovery` | Every `*.test.ts` / `*.spec.ts` file in the repo is collected by at least one test runner (ratchet: orphan list in `test-discovery-baseline.json` can only shrink) | Yes | -| `check:agent-skills-sync` | Generated agent-skills artifacts match their source catalog (no drift) | -| `check:provider-asset-provenance` | Provider logos/assets carry a recorded provenance entry | -| `lint:json` | JSON config files parse and satisfy the repo lint rules | -| `typecheck:core` | TypeScript compilation without errors (advisory warnings only) | Yes | -| `typecheck:noimplicit:core` | Strict `noImplicitAny` — forward-looking; many pre-existing call sites still need annotations | **Advisory** (`continue-on-error: true`) | -| `check:dashboard-typecheck` | `tsc` scoped to `src/app/(dashboard)/**` (#7033) — `typecheck:core`'s curated 27-file allowlist does not include any dashboard TSX, and `next build` never type-checks it either (`next.config.mjs` sets `ignoreBuildErrors: true`), so orphaned-identifier regressions there (#6625/#6909) were invisible to CI. Diffs against a frozen per-file/per-TS-code count baseline (`config/quality/dashboard-typecheck-baseline.json`, same stale-enforcement pattern as `check:known-symbols`) — only NEW errors beyond the baselined count fail the gate; ratchet down with `--update` when a pre-existing error is fixed. | Yes | +| Script (`npm run ...`) | Validates | Blocking | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| `check:node-runtime` | Node.js version is within the supported range | Yes | +| `check:cycles` | Circular imports — all `src/` + `open-sse/` modules | Yes | +| `check:route-validation:t06` | Zod schemas present on all routes (Tier 6 policy) | Yes | +| `check:any-budget:t11` | `@ts-expect-error // any` count does not exceed budget (Tier 11 catraca) | Yes | +| `check:provider-consistency` | Every provider in `providers.ts` has a matching entry in `providerRegistry.ts` (and vice-versa, within the allowlist) | Yes | +| `check:model-lifecycle` | The three hand-maintained routing tables stay consistent with the checked-in lifecycle snapshot (#11503): `FITNESS_TABLE` (`taskFitness.ts`) scores no retired id that `REGISTRY` can route; every `BUILT_IN_ALIASES` target is present in `REGISTRY` and absent from the retired-id snapshot; every retired id still in `REGISTRY` is forwarded or listed in `allowedRetiredInCatalog`; and no `DEFAULT_DEGRADATION_MAP` source or target appears retired in that snapshot. This does not prove that a model is currently served by a live upstream. Offline — compares against `config/quality/model-lifecycle.json`, refreshed by hand with `npm run quality:refresh-model-lifecycle` (network; not wired into CI). `allowedRetiredInCatalog` is a burn-down ratchet: add an entry only with a tracking issue. | Yes | +| `check:fetch-targets` | Every `fetch("/api/...")` in client-side `src/` resolves to a real `route.ts` | Yes | +| `check:deps` | All `npm install`-able deps across every `package.json` in the repo are in `dependency-allowlist.json`; new unpinned or slopsquatted packages flagged | Yes | +| `audit:deps` | `npm audit` (root + electron) — no high/critical advisories (overlaps osv `check:vuln-ratchet`; see Rationalization Backlog) | Yes | +| `check:lockfile` | `package-lock.json` integrity — https registry, integrity hashes, no host overrides | Yes | +| `check:licenses` | SPDX license allowlist for production dependencies | Yes | +| `check:tracked-artifacts` | No build artifacts / committed `node_modules` symlinks (also runs in husky pre-commit; pre-push is intentionally light — #6716) | Yes | +| `check:file-size` | No source file exceeds the per-extension cap (ratchet: frozen large files in `frozen` list) | Yes | +| `check:error-helper` | Error responses in executors/handlers use `buildErrorBody()` / `sanitizeErrorMessage()` (Hard Rule #12) | Yes | +| `check:migration-numbering` | Migration SQL files are sequentially numbered, no gaps or duplicates | Yes | +| `check:public-creds` | No literal OAuth `client_id`/`client_secret` or Firebase Web keys outside `publicCreds.ts` (Hard Rule #11) | Yes | +| `check:db-rules` | No raw SQL outside `src/lib/db/` modules; no barrel-imports from `localDb.ts` (Hard Rules #2/#5) | Yes | +| `check:known-symbols` | Provider executors, routing strategies, and translators registered in their dispatch tables match the files on disk — no orphaned or undeclared symbols | Yes | +| `check:route-guard-membership` | Every route that spawns a child process is classified by `isLocalOnlyPath()` (Hard Rules #15/#17) | Yes | +| `check:test-discovery` | Every `*.test.ts` / `*.spec.ts` file in the repo is collected by at least one test runner (ratchet: orphan list in `test-discovery-baseline.json` can only shrink) | Yes | +| `check:agent-skills-sync` | Generated agent-skills artifacts match their source catalog (no drift) | +| `check:provider-asset-provenance` | Provider logos/assets carry a recorded provenance entry | +| `lint:json` | JSON config files parse and satisfy the repo lint rules | +| `typecheck:core` | TypeScript compilation without errors (advisory warnings only) | Yes | +| `typecheck:noimplicit:core` | Strict `noImplicitAny` — forward-looking; many pre-existing call sites still need annotations | **Advisory** (`continue-on-error: true`) | +| `check:dashboard-typecheck` | `tsc` scoped to `src/app/(dashboard)/**` (#7033) — `typecheck:core`'s curated 27-file allowlist does not include any dashboard TSX, and `next build` never type-checks it either (`next.config.mjs` sets `ignoreBuildErrors: true`), so orphaned-identifier regressions there (#6625/#6909) were invisible to CI. Diffs against a frozen per-file/per-TS-code count baseline (`config/quality/dashboard-typecheck-baseline.json`, same stale-enforcement pattern as `check:known-symbols`) — only NEW errors beyond the baselined count fail the gate; ratchet down with `--update` when a pre-existing error is fixed. | Yes | ### Job: `quality-gate` diff --git a/open-sse/services/backgroundTaskDetector.ts b/open-sse/services/backgroundTaskDetector.ts index 8cbcbd3e9e..258500fbd9 100644 --- a/open-sse/services/backgroundTaskDetector.ts +++ b/open-sse/services/backgroundTaskDetector.ts @@ -45,22 +45,24 @@ const DEFAULT_DETECTION_PATTERNS = [ "label this", ]; +// Every source and target must be absent from the retired-id snapshot: a retired source is +// a dead row (checkLifecycle answers 410 before the redirect runs), while a retired target +// is normally rejected with 410 when lifecycle validation runs again after the redirect +// (unless alias resolution maps it to an accepted id). `npm run check:model-lifecycle` +// diffs this map against config/quality/model-lifecycle.json. const DEFAULT_DEGRADATION_MAP: Record = { // Premium → Cheap alternatives "claude-opus-4-6": "gemini-3-flash", "claude-opus-4-6-thinking": "gemini-3-flash", "claude-opus-4-5-20251101": "gemini-3-flash", "claude-sonnet-4-5-20250929": "gemini-3-flash", - "claude-sonnet-4-20250514": "gemini-3-flash", "claude-sonnet-4": "gemini-3-flash", "gemini-3.1-pro": "gemini-3-flash", "gemini-3.1-pro-high": "gemini-3-flash", - "gemini-3-pro-preview": "gemini-3-flash-preview", "gemini-2.5-pro": "gemini-3-flash", "gpt-4o": "gpt-4o-mini", "gpt-5": "gpt-5-mini", "gpt-5.1": "gpt-5-mini", - "gpt-5.1-codex": "gpt-5.1-codex-mini", }; // ── State ─────────────────────────────────────────────────────────────────── diff --git a/scripts/check/check-model-lifecycle.mjs b/scripts/check/check-model-lifecycle.mjs index 7bad03ec60..2e9c961ccd 100644 --- a/scripts/check/check-model-lifecycle.mjs +++ b/scripts/check/check-model-lifecycle.mjs @@ -1,19 +1,25 @@ #!/usr/bin/env node // scripts/check/check-model-lifecycle.mjs -// Gate anti-drift (#11503): as duas tabelas mantidas à mão que decidem roteamento — +// Gate anti-drift (#11503): as três tabelas mantidas à mão que decidem roteamento — // FITNESS_TABLE (open-sse/services/autoCombo/taskFitness.ts, camada 4 do task fitness) e // BUILT_IN_ALIASES (open-sse/services/modelDeprecation.ts, reescreve `body.model` em toda -// request) — apodrecem em silêncio quando o fornecedor aposenta um modelo. Em +// request), além de DEFAULT_DEGRADATION_MAP (backgroundTaskDetector.ts) — apodrecem em +// silêncio quando o fornecedor aposenta um modelo. Em // release/v3.8.51 o resultado foi uma inversão de ranking (modelo morto 0.98 vs flagship -// vivo 0.50) e aliases que garantiam 404. Este gate compara as duas contra o snapshot de +// vivo 0.50) e aliases apontando para ids obsoletos. Este gate compara as três contra o snapshot de // ciclo de vida em config/quality/model-lifecycle.json (sem rede; regenerar com // `npm run quality:refresh-model-lifecycle`). // -// Três checagens, todas somadas antes do exit — nenhuma aborta as outras: +// Quatro checagens, todas somadas antes do exit — nenhuma aborta as outras: // (a) nenhum padrão do FITNESS_TABLE pontua um id aposentado que o catálogo roteia; // (b) nenhum alvo de BUILT_IN_ALIASES está aposentado ou ausente do catálogo; // (c) todo id aposentado ainda presente no REGISTRY tem encaminhamento em -// BUILT_IN_ALIASES ou consta em `allowedRetiredInCatalog` (a catraca a queimar). +// BUILT_IN_ALIASES ou consta em `allowedRetiredInCatalog` (a catraca a queimar); +// (d) nenhuma linha de DEFAULT_DEGRADATION_MAP (open-sse/services/backgroundTaskDetector.ts) +// tem origem ou destino aposentado. A origem aposentada é linha morta: checkLifecycle +// devolve 410 antes de resolveBackgroundTaskRedirect rodar. O destino aposentado é o +// normalmente rejeitado com 410 quando o ciclo de vida é validado novamente após o +// redirecionamento; a resolução de alias ainda pode convertê-lo em um id aceito. // // (a) é deliberadamente restrita aos ids ROTEÁVEIS: linhas versionadas legítimas como // `gpt-4o` também casam com ids aposentados que o catálogo nunca serviu @@ -88,6 +94,22 @@ export function findUnforwardedRetiredIds(routableRetiredIds, aliases, allowlist .map((id) => `${id} is retired but still routable with no BUILT_IN_ALIASES forward`); } +/** (d) Linhas de DEFAULT_DEGRADATION_MAP com origem ou destino aposentado. */ +export function findRetiredDegradationRows(degradationMap, retiredIds) { + const violations = []; + for (const [source, target] of Object.entries(degradationMap ?? {})) { + if (isRetiredId(source, retiredIds)) { + violations.push( + `${source} → ${target} (the vendor has retired the source id; checkLifecycle rejects it before the redirect runs)` + ); + } + if (isRetiredId(target, retiredIds)) { + violations.push(`${source} → ${target} (the vendor has retired the target id)`); + } + } + return violations; +} + export function readSnapshot(snapshotPath = SNAPSHOT_PATH) { const snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf8")); const retiredIds = new Set( @@ -102,12 +124,18 @@ async function loadProductionTables() { // Nenhum gate pode migrar o banco do operador: taskFitness.ts importa src/lib/db/core.ts, // então DATA_DIR aponta para um diretório descartável ANTES do import dinâmico. process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-lifecycle-gate-")); - const [{ REGISTRY }, { getStaticFitnessTableScore }, { getBuiltInAliases }] = await Promise.all([ + const [ + { REGISTRY }, + { getStaticFitnessTableScore }, + { getBuiltInAliases }, + { getDefaultDegradationMap }, + ] = await Promise.all([ import(pathToFileURL(path.join(ROOT, "open-sse/config/providers/index.ts")).href), import(pathToFileURL(path.join(ROOT, "open-sse/services/autoCombo/taskFitness.ts")).href), import(pathToFileURL(path.join(ROOT, "open-sse/services/modelDeprecation.ts")).href), + import(pathToFileURL(path.join(ROOT, "open-sse/services/backgroundTaskDetector.ts")).href), ]); - return { REGISTRY, getStaticFitnessTableScore, getBuiltInAliases }; + return { REGISTRY, getStaticFitnessTableScore, getBuiltInAliases, getDefaultDegradationMap }; } function report(label, violations, hint) { @@ -125,11 +153,13 @@ function report(label, violations, hint) { async function main() { const { snapshot, retiredIds } = readSnapshot(); - const { REGISTRY, getStaticFitnessTableScore, getBuiltInAliases } = await loadProductionTables(); + const { REGISTRY, getStaticFitnessTableScore, getBuiltInAliases, getDefaultDegradationMap } = + await loadProductionTables(); const catalogIds = collectCatalogIds(REGISTRY); const routableRetired = catalogIds.filter((id) => isRetiredId(id, retiredIds)).sort(); const aliases = getBuiltInAliases(); + const degradationMap = getDefaultDegradationMap(); let failures = 0; failures += report( @@ -138,7 +168,7 @@ async function main() { "drop the row from FITNESS_TABLE in open-sse/services/autoCombo/taskFitness.ts, or replace it with the versioned id of the live successor." ); failures += report( - `all ${Object.keys(aliases).length} BUILT_IN_ALIASES targets are live catalog models`, + `all ${Object.keys(aliases).length} BUILT_IN_ALIASES targets are present in REGISTRY and absent from the retired-id snapshot`, findBadAliasTargets(aliases, catalogIds, retiredIds), "point the alias at the replacement the vendor publishes (see `sources` in config/quality/model-lifecycle.json). Never invent a target." ); @@ -148,8 +178,14 @@ async function main() { "add a BUILT_IN_ALIASES forward to the vendor's replacement, remove the model from the provider catalog, or (last resort) add the id to `allowedRetiredInCatalog` in config/quality/model-lifecycle.json with a tracking issue." ); + failures += report( + `none of the ${Object.keys(degradationMap).length} DEFAULT_DEGRADATION_MAP rows names a retired id`, + findRetiredDegradationRows(degradationMap, retiredIds), + "drop the row from DEFAULT_DEGRADATION_MAP in open-sse/services/backgroundTaskDetector.ts (a retired source can never reach the redirect), or point a retired target at the replacement the vendor publishes (see `sources` in config/quality/model-lifecycle.json)." + ); + if (failures) { - console.error(`[model-lifecycle] FAIL — ${failures} violation(s) across 3 check(s).`); + console.error(`[model-lifecycle] FAIL — ${failures} violation(s) across 4 check(s).`); process.exit(1); } console.log( diff --git a/tests/unit/check-model-lifecycle-gate.test.ts b/tests/unit/check-model-lifecycle-gate.test.ts index d717ffabe4..3a9d649f7f 100644 --- a/tests/unit/check-model-lifecycle-gate.test.ts +++ b/tests/unit/check-model-lifecycle-gate.test.ts @@ -2,7 +2,7 @@ * Unit coverage for the #11503 drift gate (`scripts/check/check-model-lifecycle.mjs`). * * The gate's value is that it goes red when a hand-maintained routing table starts - * pointing at a model the vendor retired, so each of its three checks is exercised here + * pointing at a model the vendor retired, so each of its four checks is exercised here * against small fixtures rather than against the live catalog (which would make the test * a duplicate of the gate run itself, and red for reasons unrelated to the logic). */ @@ -14,6 +14,7 @@ import { findRetiredFitnessRows, findBadAliasTargets, findUnforwardedRetiredIds, + findRetiredDegradationRows, } from "../../scripts/check/check-model-lifecycle.mjs"; const RETIRED = new Set(["dead-model-1", "dead-model-2", "gpt-5.2-codex"]); @@ -93,3 +94,32 @@ describe("check-model-lifecycle: (c) routable retired ids", () => { ); }); }); + +describe("check-model-lifecycle: (d) DEFAULT_DEGRADATION_MAP rows", () => { + it("flags a retired source id as a dead row", () => { + const violations = findRetiredDegradationRows({ "dead-model-1": "live-1" }, RETIRED); + assert.equal(violations.length, 1); + assert.match(violations[0], /retired the source id; checkLifecycle rejects it/); + }); + + it("flags a retired target id", () => { + const violations = findRetiredDegradationRows({ "live-1": "dead-model-1" }, RETIRED); + assert.equal(violations.length, 1); + assert.match(violations[0], /retired the target id/); + }); + + it("reports both ends when source and target are retired", () => { + const violations = findRetiredDegradationRows({ "dead-model-1": "dead-model-2" }, RETIRED); + assert.equal(violations.length, 2); + }); + + it("treats a vendor-prefixed source as retired when its bare form is", () => { + const violations = findRetiredDegradationRows({ "openai/gpt-5.2-codex": "live-1" }, RETIRED); + assert.equal(violations.length, 1); + }); + + it("passes for a map of live ids", () => { + assert.deepEqual(findRetiredDegradationRows({ "live-1": "live-2" }, RETIRED), []); + assert.deepEqual(findRetiredDegradationRows({}, RETIRED), []); + }); +}); diff --git a/tests/unit/model-lifecycle-degradation-map.test.ts b/tests/unit/model-lifecycle-degradation-map.test.ts new file mode 100644 index 0000000000..0b59ab732a --- /dev/null +++ b/tests/unit/model-lifecycle-degradation-map.test.ts @@ -0,0 +1,56 @@ +/** + * Follow-up to #11503 / #11507: `DEFAULT_DEGRADATION_MAP` (backgroundTaskDetector.ts) is the + * third hand-maintained routing table that names model ids, and it was outside the + * retired-model gate. A retired *source* is a dead row — `checkLifecycle` answers 410 + * `model_shutdown` before `resolveBackgroundTaskRedirect` runs — and a retired *target* + * is normally rejected with 410 when lifecycle validation runs again after the redirect, + * unless alias resolution maps it to an accepted id. + * + * Table-driven over the production default map and the checked-in lifecycle snapshot, mirroring + * `model-deprecation-aliases-11503.test.ts`, so a new dead row fails by name. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { getDefaultDegradationMap } from "../../open-sse/services/backgroundTaskDetector.ts"; +import { isVendorRetiredId } from "../../open-sse/services/modelLifecycle.ts"; + +const lifecycle = JSON.parse( + readFileSync( + fileURLToPath(new URL("../../config/quality/model-lifecycle.json", import.meta.url)), + "utf8" + ) +) as { retired: Record }; + +const retiredIds = new Set( + Object.entries(lifecycle.retired) + .filter(([, entry]) => entry.status === "retired") + .map(([id]) => id.toLowerCase()) +); + +describe("DEFAULT_DEGRADATION_MAP names no retired model id", () => { + const rows = Object.entries(getDefaultDegradationMap()); + + it("has rows to check", () => { + assert.ok(rows.length > 0); + }); + + for (const [source, target] of rows) { + it(`degrades from ${source}, an id the vendor has not retired`, () => { + assert.ok( + !retiredIds.has(source.toLowerCase()), + `"${source}" → "${target}" is dead: the vendor has retired "${source}", so checkLifecycle rejects the request before the background redirect runs` + ); + assert.equal(isVendorRetiredId(source), false); + }); + + it(`degrades ${source} to ${target}, an id the vendor has not retired`, () => { + assert.ok( + !retiredIds.has(target.toLowerCase()), + `"${source}" → "${target}" forwards background tasks to "${target}", which the vendor has retired` + ); + assert.equal(isVendorRetiredId(target), false); + }); + } +}); From afd2d993e632637683ab62403f4c5e9b38e04ca5 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:21 +0200 Subject: [PATCH 14/20] fix(rerank): clamp Voyage top_k and honor NVIDIA return_documents (#12523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both defects are real adapter bugs: `top_k` computed from the unfiltered array after the adapter drops empty strings makes Voyage reject a request that is valid under the Cohere-style contract this endpoint exposes. Good that the NVIDIA `return_documents` half rides along rather than waiting. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12523-rerank-topk-and-return-documents.md | 1 + open-sse/handlers/rerank.ts | 9 +++- tests/unit/rerank-providers-5332.test.ts | 34 ++++++++++++++ tests/unit/rerank-voyage-7809.test.ts | 44 +++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/12523-rerank-topk-and-return-documents.md diff --git a/changelog.d/fixes/12523-rerank-topk-and-return-documents.md b/changelog.d/fixes/12523-rerank-topk-and-return-documents.md new file mode 100644 index 0000000000..88b1a23726 --- /dev/null +++ b/changelog.d/fixes/12523-rerank-topk-and-return-documents.md @@ -0,0 +1 @@ +- **fix(rerank):** clamp Voyage `top_k` to the documents actually sent after empty-string filtering, and honor `return_documents: false` in the NVIDIA response adapter (#12523 — thanks @pacocartones) diff --git a/open-sse/handlers/rerank.ts b/open-sse/handlers/rerank.ts index 45ab3c2bee..3963f7d154 100644 --- a/open-sse/handlers/rerank.ts +++ b/open-sse/handlers/rerank.ts @@ -73,6 +73,10 @@ function buildAuthHeader(providerConfig, token) { // strings (whitespace-only documents are accepted and ranked upstream). We // filter out exact empty strings and track original indices implicitly via the // response adapter, which reconstructs the map from options.documents (#7809). + // `top_k` is clamped to the number of documents actually sent: the handler + // defaults `top_n` to the caller's *unfiltered* document count, so dropping an + // empty string would otherwise ask Voyage to rank more documents than it got, + // and Voyage rejects `top_k > documents.length` with HTTP 400. // `return_documents` is always forced off upstream: Voyage echoes documents as // plain strings (not Cohere's {text}), so we never rely on the echo — document // text is always synthesized locally from the caller's originals (#7811). @@ -84,7 +88,7 @@ function buildAuthHeader(providerConfig, token) { model: body.model, query: body.query, documents: docTexts, - top_k: body.top_n || docTexts.length, + top_k: Math.min(body.top_n || docTexts.length, docTexts.length), return_documents: false, }; } @@ -101,12 +105,13 @@ function buildAuthHeader(providerConfig, token) { options: RerankResponseOptions = {} ) { if (providerConfig.format === "nvidia") { + const returnDocuments = options.return_documents !== false; return { id: data.id != null ? String(data.id) : `rerank-${Date.now()}`, results: (data.rankings || []).map((r) => ({ index: r.index, relevance_score: r.logit || r.score || 0, - document: { text: r.text || "" }, + ...(returnDocuments ? { document: { text: r.text || "" } } : {}), })), meta: { api_version: { version: "2" }, diff --git a/tests/unit/rerank-providers-5332.test.ts b/tests/unit/rerank-providers-5332.test.ts index 20a0ad74ef..c7e39a1fb9 100644 --- a/tests/unit/rerank-providers-5332.test.ts +++ b/tests/unit/rerank-providers-5332.test.ts @@ -69,3 +69,37 @@ test("#5332 deepinfra response omits document text when return_documents=false", assert.equal(out.results[0].document, undefined); assert.equal(out.results[0].index, 1); }); + +// ─── NVIDIA must honor return_documents like its deepinfra/voyage siblings ── + +test("#5332 nvidia response omits document text when return_documents=false", () => { + const cfg = getRerankProvider("nvidia"); + const out = transformResponseFromProvider( + cfg, + { id: "r1", rankings: [{ index: 0, logit: 0.8, text: "a" }] }, + { documents: ["a"], return_documents: false } + ); + assert.equal(out.results[0].document, undefined); + assert.equal(out.results[0].index, 0); + assert.equal(out.results[0].relevance_score, 0.8); +}); + +test("#5332 nvidia response includes document text when return_documents is true", () => { + const cfg = getRerankProvider("nvidia"); + const out = transformResponseFromProvider( + cfg, + { id: "r1", rankings: [{ index: 1, logit: 0.4, text: "b" }] }, + { documents: ["a", "b"], return_documents: true } + ); + assert.equal(out.results[0].document.text, "b"); +}); + +test("#5332 nvidia response includes document text when return_documents is omitted", () => { + const cfg = getRerankProvider("nvidia"); + const out = transformResponseFromProvider( + cfg, + { id: "r1", rankings: [{ index: 0, logit: 0.9, text: "a" }] }, + { documents: ["a"] } + ); + assert.equal(out.results[0].document.text, "a"); +}); diff --git a/tests/unit/rerank-voyage-7809.test.ts b/tests/unit/rerank-voyage-7809.test.ts index 208fad237c..059c964d25 100644 --- a/tests/unit/rerank-voyage-7809.test.ts +++ b/tests/unit/rerank-voyage-7809.test.ts @@ -223,3 +223,47 @@ test("#7809 voyage response adapter handles empty data array", () => { const out = transformResponseFromProvider(cfg, { data: [] }, { documents: ["a", "b"] }); assert.deepEqual(out.results, []); }); + +// ─── top_k must never exceed the surviving document count ────────────────── +// The handler normalizes `top_n: top_n || documents.length` BEFORE the adapter +// runs, so a caller that omits top_n and sends an exact empty string yields +// top_k > documents.length — which Voyage rejects with HTTP 400. + +test("#7809 voyage request adapter clamps top_k to the surviving document count", () => { + const cfg = getRerankProvider("voyage-ai"); + const out = transformRequestForProvider(cfg, { + model: "rerank-2.5-lite", + query: "teste", + documents: ["a", "", "b"], + // Mirrors the handler's `top_n: top_n || documents.length` when the caller omits top_n. + top_n: 3, + return_documents: true, + }); + assert.deepEqual(out.documents, ["a", "b"]); + assert.equal(out.top_k, 2, "top_k must not exceed the number of documents actually sent"); +}); + +test("#7809 voyage request adapter clamps an explicit oversized top_n", () => { + const cfg = getRerankProvider("voyage-ai"); + const out = transformRequestForProvider(cfg, { + model: "rerank-2.5-lite", + query: "teste", + documents: ["a", "", "", "b"], + top_n: 10, + return_documents: true, + }); + assert.deepEqual(out.documents, ["a", "b"]); + assert.equal(out.top_k, 2); +}); + +test("#7809 voyage request adapter keeps a legitimate top_n below the document count", () => { + const cfg = getRerankProvider("voyage-ai"); + const out = transformRequestForProvider(cfg, { + model: "rerank-2.5-lite", + query: "teste", + documents: ["a", "b", "c"], + top_n: 2, + return_documents: true, + }); + assert.equal(out.top_k, 2); +}); From 7cd2fab25393a18dcb33088e4fe54a88bbbdffab Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:25 +0200 Subject: [PATCH 15/20] fix(routing): honor edited custom-node API type (#12358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forwarding only an explicit custom-model DB override as `modelInfo.targetFormat` is the key distinction — it lets chat core's credential-aware resolution pick the live connection setting instead of the format baked into the node id at creation, which is exactly what #11884 was about. I rebaselined the integration test file for the new case. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12358-custom-node-api-type-precedence.md | 1 + config/quality/file-size-baseline.json | 3 +- src/sse/handlers/chat.ts | 7 +- src/sse/handlers/chatHelpers.ts | 10 ++- tests/integration/chat-pipeline.test.ts | 88 +++++++++++++++++++ tests/unit/chat-helpers.test.ts | 56 ++++++++++++ 6 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/12358-custom-node-api-type-precedence.md diff --git a/changelog.d/fixes/12358-custom-node-api-type-precedence.md b/changelog.d/fixes/12358-custom-node-api-type-precedence.md new file mode 100644 index 0000000000..739017ad26 --- /dev/null +++ b/changelog.d/fixes/12358-custom-node-api-type-precedence.md @@ -0,0 +1 @@ +- **fix(routing):** custom OpenAI-compatible nodes now honor the saved Chat/Responses API type after edits instead of letting the node's original ID prefix override the live connection setting ([#11884](https://github.com/diegosouzapw/OmniRoute/issues/11884)). diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index f855229516..0b46fbf4a6 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_09_11_12358_chat_pipeline_custom_node": "PR #12358 own test growth: tests/integration/chat-pipeline.test.ts 1648->1736 (+88). One new integration case, \"#11884 chat pipeline sends a custom node's edited Chat API type upstream\": it seeds a custom OpenAI-compatible node with an edited Chat/Responses API type, stubs fetch, drives handleChatCore and asserts the upstream request carries the live connection setting rather than the format baked into the node id at creation. Irreducible at this layer — the point of the test is the full route-to-upstream path, which is what #11884 regressed. Nothing else in the file changed. Covered by the case itself plus tests/unit/chat-helpers.test.ts (28/28).", "_rebaseline_2026_09_10_12975_rotation_correlation_id": "PR #12975 own growth: open-sse/executors/base.ts 1751->1753 (+2) and open-sse/handlers/chatCore.ts 6021->6024 (+3). The opencode rotation lines carry the request correlationId: one optional ExecuteInput field and one correlationId argument at each of the three executor.execute call sites in handleChatCore. Irreducible plumbing at existing call sites; the rotation logic itself lives in open-sse/executors/opencode.ts and the new leaf predicates (under cap). Covered by tests/unit/opencode-transient-rotation.test.ts and tests/unit/chat-correlation-id-exhaustion.test.ts.", "_rebaseline_2026_09_11_mergebatch_v3851_maxmad_opencode": "/merge-batch 2026-09-11 (v3.8.51), PRs #13141, #13146 and #12975 by maxmad64bis. src/sse/services/auth.ts 3450->3488 (+38): #13146 adds the narrow ruleScope===model branch to markAccountUnavailable (gated on status 400; every other status keeps its path) plus the HONORS_RULE_LOCK_SCOPE_PROVIDERS opencode entry, taking it to 3464; #12975 then adds buildExhaustionOptions so the exhaustion log lines carry the request correlationId (+24). open-sse/services/accountFallback.ts 2467->2468 (+1): #13141 routes hasFutureRateLimitUntil through the tolerant epoch normalizer; #13146 is net zero there (+16/-16). open-sse/executors/base.ts 1751->1753 (+2): #12975 adds the optional ExecuteInput.correlationId field with its doc comment. src/sse/handlers/chat.ts is NOT rebaselined: #12975 threads correlationId through the three executor call sites (+2) but the file lands at 2452, still under its existing 2458 freeze. open-sse/utils/stream.ts is deliberately NOT rebaselined either: it is already 3115 > 3098 on the pure tip with zero contribution from this batch (base-red #12732, owned by /sweep-reds). No new branching beyond the two guarded branches named above. Covered by tests/unit/combo-predicates-epoch-cooldown.test.ts, opencode-400-model-unavailable.test.ts, agentrouter-error-rules.test.ts, opencode-transient-rotation.test.ts and chat-correlation-id-exhaustion.test.ts.", "_rebaseline_2026_09_10_mergebatch_v3851_greenpt_eurouter": "/merge-batch 2026-09-10 (v3.8.51), PRs #13024 (GreenPT, closes #12986) and #13025 (EURouter, closes #12985) by ntdatt812: src/shared/constants/providers/apikey/gateways.ts 1462->1502 (+40 = two APIKEY_PROVIDERS_GATEWAYS catalog entries, declarative data only: id/alias/name/icon/color/website plus the hasFree=false rationale comments and the apiHint copy each PR verified). No logic and no new branching. Same god-file no-split rationale as every prior gateways.ts rebaseline (#11786 seekai, #10987 logfare, #10668 tabitoken, #10531 freebuff, #11631 1min.ai): the file header says it is pure data merged by apikey/index.ts via spread, and it is already split into 6 family files under apikey/, so splitting a catalog for two entries would violate the semantic-families rule rather than help. Both entries are deliberately conservative (models: [] with passthroughModels, no tool/vision capability declared, hasFree false), so the growth is the entry itself, not claims. EURouter is in AGGREGATOR_PROVIDER_IDS because it routes to third-party upstreams; GreenPT is not because it serves its own inference. Covered by tests/unit/greenpt-provider.test.ts and tests/unit/eurouter-provider.test.ts.", @@ -220,7 +221,7 @@ "_rebaseline_2026_08_24_video_bridge_fu01_fu03_fu04_result_cache_tests": "PRs #11362 (FU-01 cache hardening) + #11382 (FU-03 visual dedup policy identity) + #11383 (FU-04 focused analysis mode) own test growth: videoBridgeResultCache.test.ts <1000->1040, +40 (sum of three stacked PRs boarded together in the same merge-batch, each adding its own cache-identity assertions on the shared result-cache seam). Owner pre-authorized rebaseline for legitimate PR growth (2026-08-19 directive).", "_rebaseline_basered_codebuddy_cn": "Base-red fix (#4664 CodeBuddy CN): oauth-providers-config.test.ts 867->870 (+3) to align the EXPECTED provider list/config with the codebuddy-cn provider that #4664 added to the registry without updating this test (it asserts 'exactly once').", "_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.", - "tests/integration/chat-pipeline.test.ts": 1648, + "tests/integration/chat-pipeline.test.ts": 1736, "tests/unit/account-fallback-service.test.ts": 2056, "tests/unit/batch_api.test.ts": 1345, "tests/unit/cc-compatible-provider.test.ts": 1225, diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 0a5b3fafbe..fcd565133a 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -1489,6 +1489,7 @@ async function handleSingleModelChat( model, sourceFormat, targetFormat, + customModelTargetFormat, extendedContext, apiFormat, } = resolved; @@ -1940,7 +1941,11 @@ async function handleSingleModelChat( runtimeOptions.comboExecutionKey ?? runtimeOptions.comboStepId ?? null, extendedContext, modelApiFormat: apiFormat, - modelTargetFormat: targetFormat, + // Only a model's explicit DB override may cross this boundary as + // modelInfo.targetFormat. The effective targetFormat above was + // resolved without credentials; forwarding it would let a stale + // provider-id fallback override the credential-aware resolution. + modelTargetFormat: customModelTargetFormat, providerProfile, cachedSettings: runtimeOptions.cachedSettings, skipUpstreamRetry: runtimeOptions.skipUpstreamRetry ?? false, diff --git a/src/sse/handlers/chatHelpers.ts b/src/sse/handlers/chatHelpers.ts index 17e3a0f08a..c29300b30d 100644 --- a/src/sse/handlers/chatHelpers.ts +++ b/src/sse/handlers/chatHelpers.ts @@ -338,7 +338,15 @@ export async function resolveModelOrError( log.info("ROUTING", `Provider: ${provider}, Model: ${model}${ctxTag}`); } - return { provider, model, sourceFormat, targetFormat, extendedContext, apiFormat }; + return { + provider, + model, + sourceFormat, + targetFormat, + customModelTargetFormat, + extendedContext, + apiFormat, + }; } export async function checkPipelineGates( diff --git a/tests/integration/chat-pipeline.test.ts b/tests/integration/chat-pipeline.test.ts index 191462d93c..e2303c86b9 100644 --- a/tests/integration/chat-pipeline.test.ts +++ b/tests/integration/chat-pipeline.test.ts @@ -21,6 +21,7 @@ const { skillRegistry } = await import("../../src/lib/skills/registry.ts"); const { skillExecutor } = await import("../../src/lib/skills/executor.ts"); const { encodeSkillToolName } = await import("../../src/lib/skills/injection.ts"); const { handleChat } = await import("../../src/sse/handlers/chat.ts"); +const providerNodeRoute = await import("../../src/app/api/provider-nodes/[id]/route.ts"); const { initTranslators } = await import("../../open-sse/translator/index.ts"); const { clearInflight } = await import("../../open-sse/services/requestDedup.ts"); const { setCliCompatProviders } = await import("../../open-sse/config/cliFingerprints.ts"); @@ -550,6 +551,93 @@ test("chat pipeline handles OpenAI passthrough with valid API key auth", async ( assert.equal(json.choices[0].message.content, "OpenAI passthrough"); }); +test("#11884 chat pipeline sends a custom node's edited Chat API type upstream", async () => { + // Mirror POST /api/provider-nodes: the generated node id embeds the API type chosen at + // creation time, so a node created as Responses keeps "responses" in its id forever. + const providerId = "openai-compatible-responses-11884"; + const prefix = "edited-node-11884"; + const baseUrl = "https://edited-node-11884.example.invalid/v1"; + const nodeName = "Edited node 11884"; + await providersDb.createProviderNode({ + id: providerId, + type: "openai-compatible", + name: nodeName, + prefix, + apiType: "responses", + baseUrl, + }); + await seedConnection(providerId, { + apiKey: "sk-edited-node-11884", + providerSpecificData: { baseUrl, apiType: "responses" }, + }); + + // The operator edits the node from Responses to Chat through the real route, which also + // rewrites the connection's saved apiType. + const editResponse = await providerNodeRoute.PUT( + new Request(`http://localhost/api/provider-nodes/${providerId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: nodeName, prefix, apiType: "chat", baseUrl }), + }), + { params: Promise.resolve({ id: providerId }) } + ); + assert.equal(editResponse.status, 200); + const [connection] = (await providersDb.getProviderConnections({ + provider: providerId, + })) as Array<{ + providerSpecificData?: { apiType?: unknown }; + }>; + assert.equal(connection?.providerSpecificData?.apiType, "chat"); + + const apiKey = await seedApiKey(); + const fetchCalls: FetchCall[] = []; + globalThis.fetch = async (url, init: RequestInit = {}) => { + const call: FetchCall = { + url: String(url), + method: init.method || "GET", + headers: toPlainHeaders(init.headers), + body: init.body ? JSON.parse(String(init.body)) : null, + }; + fetchCalls.push(call); + if (!call.url.startsWith(baseUrl)) { + throw new Error(`unexpected upstream call: ${call.method} ${call.url}`); + } + return buildOpenAIResponse("Edited node reply", "edited-model"); + }; + + const response = await handleChat( + buildRequest({ + authKey: apiKey.key, + body: { + model: `${prefix}/edited-model`, + stream: false, + messages: [{ role: "user", content: "Hello edited node" }], + }, + }) + ); + + const json = (await response.json()) as { choices: Array<{ message: { content: string } }> }; + assert.ok(fetchCalls.length >= 1, "expected an upstream request"); + const upstream = fetchCalls[0]; + assert.equal(upstream.method, "POST"); + assert.equal(upstream.url, `${baseUrl}/chat/completions`); + assert.equal(upstream.headers.Authorization, "Bearer sk-edited-node-11884"); + assert.deepEqual( + upstream.body.messages, + [{ role: "user", content: "Hello edited node" }], + "the saved Chat API type must produce a Chat Completions body" + ); + assert.equal( + upstream.body.input, + undefined, + "the stale Responses API type from the node id must not shape the upstream body" + ); + assert.equal(upstream.body.model, "edited-model"); + assert.equal(fetchCalls.length, 1, "exactly one upstream request"); + assert.equal(response.status, 200); + assert.equal(json.choices[0].message.content, "Edited node reply"); +}); + test("chat pipeline persists Codex responses cache and reasoning tokens to call logs", async () => { await seedConnection("codex", { apiKey: "sk-codex-primary" }); const fetchCalls = []; diff --git a/tests/unit/chat-helpers.test.ts b/tests/unit/chat-helpers.test.ts index 710bb6f21c..21ac50fa27 100644 --- a/tests/unit/chat-helpers.test.ts +++ b/tests/unit/chat-helpers.test.ts @@ -24,6 +24,9 @@ const { getCircuitBreaker, resetAllCircuitBreakers, STATE } = await import("../../src/shared/utils/circuitBreaker.ts"); // DATA_DIR must be fixed before these modules load; keep this test seam dynamic. const { setTlsClientForTest } = await import("../../open-sse/utils/proxyFetch.ts"); +const { resolveChatCoreTargetFormat } = + await import("../../open-sse/handlers/chatCore/targetFormat.ts"); +const { FORMATS } = await import("../../open-sse/translator/formats.ts"); type ApiErrorJson = { error?: { @@ -259,6 +262,59 @@ test("resolveModelOrError honors a custom-model targetFormat override even when assert.equal(result.targetFormat, "claude"); }); +test("#11884 configured Chat API type wins after custom-node model resolution", async () => { + const provider = "openai-compatible-responses-11884"; + const prefix = "custom-chat-11884"; + const model = "chat-only-model"; + + await providersDb.createProviderNode({ + id: provider, + type: "openai-compatible", + name: "Custom Chat 11884", + prefix, + apiType: "chat", + baseUrl: "https://chat-only.example.invalid/v1", + }); + const connection = await seedConnection(provider, { + providerSpecificData: { apiType: "chat" }, + }); + const modelsDb = await import("../../src/lib/db/models.ts"); + await modelsDb.addCustomModel(provider, model, "Chat-only model", "manual", "chat-completions", [ + "chat", + ]); + + const firstResolution = await resolveModelOrError( + `${prefix}/${model}`, + { model: `${prefix}/${model}`, messages: [{ role: "user", content: "hello" }] }, + "/v1/chat/completions" + ); + assert.equal(firstResolution.error, undefined); + + // Before #11884's fix the resolver exposed only its credential-blind effective + // targetFormat, so the dispatcher necessarily forwarded that value as though it + // were a model override. The fixed contract exposes the explicit model override + // separately; keep the fallback here so this regression test still exercises the + // broken production path when run against the parent revision. + const forwardedModelOverride = + "customModelTargetFormat" in firstResolution + ? firstResolution.customModelTargetFormat + : firstResolution.targetFormat; + const finalResolution = resolveChatCoreTargetFormat({ + provider: firstResolution.provider, + resolvedModel: firstResolution.model, + apiFormat: firstResolution.apiFormat, + sourceFormat: firstResolution.sourceFormat, + customModelTargetFormat: forwardedModelOverride, + providerSpecificData: connection.providerSpecificData, + }); + + assert.equal( + finalResolution.targetFormat, + FORMATS.OPENAI, + "the stored Chat API type must not be shadowed by a stale Responses fallback" + ); +}); + test("checkPipelineGates blocks providers with an open circuit breaker", async () => { const breaker = getCircuitBreaker("openai"); breaker.state = STATE.OPEN; From 6caf836092d4b1e31c5bb3f926f084623e051eb5 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:28 +0200 Subject: [PATCH 16/20] fix(providers): include Agnes model in video polling (#12356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Straightforward and complete: polling by `video_id` without `model_name` could not identify the job, and URL-encoding in the shared builder covers the custom-provider preset as well as the built-in. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- changelog.d/fixes/12356-agnes-video-poll-model-name.md | 1 + open-sse/handlers/videoGeneration/job.ts | 6 ++++-- tests/unit/agnes-provider.test.ts | 4 ++-- tests/unit/video-custom-provider-route.test.ts | 9 +++++++-- 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 changelog.d/fixes/12356-agnes-video-poll-model-name.md diff --git a/changelog.d/fixes/12356-agnes-video-poll-model-name.md b/changelog.d/fixes/12356-agnes-video-poll-model-name.md new file mode 100644 index 0000000000..e756da7686 --- /dev/null +++ b/changelog.d/fixes/12356-agnes-video-poll-model-name.md @@ -0,0 +1 @@ +- **fix(providers):** include the submitted Agnes video model when polling by `video_id` diff --git a/open-sse/handlers/videoGeneration/job.ts b/open-sse/handlers/videoGeneration/job.ts index 030fe97a48..17308e77b8 100644 --- a/open-sse/handlers/videoGeneration/job.ts +++ b/open-sse/handlers/videoGeneration/job.ts @@ -121,7 +121,7 @@ const VIDEO_JOB_PRESETS: Record = { }), }, taskIdPath: "video_id", - poll: { pathTemplate: "/agnesapi?video_id={taskId}" }, + poll: { pathTemplate: "/agnesapi?video_id={taskId}&model_name={model}" }, statusPath: "status", statusDone: ["completed"], statusFailed: ["failed"], @@ -273,7 +273,9 @@ export async function handleVideoJobGeneration({ for (let attempt = 1; attempt <= maxPolls; attempt += 1) { await sleep(pollInterval); - const pollUrl = `${baseUrl}${preset.poll.pathTemplate.replace("{taskId}", encodeURIComponent(taskId))}`; + const pollUrl = `${baseUrl}${preset.poll.pathTemplate + .replace("{taskId}", encodeURIComponent(taskId)) + .replace("{model}", encodeURIComponent(model))}`; const pollResult = await fetchJson(pollUrl, { method: "GET", headers: buildJobHeaders(preset, credentials), diff --git a/tests/unit/agnes-provider.test.ts b/tests/unit/agnes-provider.test.ts index bc15ab9e8c..2b0930eaa6 100644 --- a/tests/unit/agnes-provider.test.ts +++ b/tests/unit/agnes-provider.test.ts @@ -224,7 +224,7 @@ test("agnes registers Video V2.0 on the current video_id job contract", () => { assert.ok(getAllVideoModels().some((model) => model.id === "agnes/agnes-video-v2.0")); }); -test("agnes Video V2.0 submits with Bearer auth and polls by video_id", async () => { +test("agnes Video V2.0 submits with Bearer auth and polls by video_id and model_name", async () => { const originalFetch = globalThis.fetch; const originalSetTimeout = globalThis.setTimeout; const calls: Array<{ @@ -309,7 +309,7 @@ test("agnes Video V2.0 submits with Bearer auth and polls by video_id", async () }, }); assert.deepEqual(calls[1], { - url: "https://apihub.agnes-ai.com/agnesapi?video_id=video-123", + url: "https://apihub.agnes-ai.com/agnesapi?video_id=video-123&model_name=agnes-video-v2.0", method: "GET", headers: { "Content-Type": "application/json", diff --git a/tests/unit/video-custom-provider-route.test.ts b/tests/unit/video-custom-provider-route.test.ts index 9c10d6f2cb..a963b7710a 100644 --- a/tests/unit/video-custom-provider-route.test.ts +++ b/tests/unit/video-custom-provider-route.test.ts @@ -213,7 +213,9 @@ test("video route dispatches submit→poll job flow for custom model with agnes- headers: { "content-type": "application/json" }, }); } - if (stringUrl === "https://custom.example.com/agnesapi?video_id=video-123") { + if ( + stringUrl === "https://custom.example.com/agnesapi?video_id=video-123&model_name=job-video-v1" + ) { return createResponse( JSON.stringify({ status: "completed", @@ -256,7 +258,10 @@ test("video route dispatches submit→poll job flow for custom model with agnes- prompt: "a cat playing piano", }); assert.equal(calls[1].method, "GET"); - assert.equal(calls[1].url, "https://custom.example.com/agnesapi?video_id=video-123"); + assert.equal( + calls[1].url, + "https://custom.example.com/agnesapi?video_id=video-123&model_name=job-video-v1" + ); }); test("video route returns 502 when job preset reports failed status", async () => { From 16c68bad4957e880da951fcc7554e3cfb53a2ecf Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:42:32 +0200 Subject: [PATCH 17/20] feat(gamification): pay the documented streak and badge XP rewards (#12522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `XP_REWARDS` documented `streak_bonus` and `badge_unlock` and the pipeline paid neither — closing that gap is right. The idempotency design carries it: `advanceStreak()` reporting `extended` only on the call that moves the record to today is what keeps a same-day repeat from paying twice. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass. --- .../12522-gamification-streak-badge-xp.md | 1 + src/lib/db/gamification.ts | 29 ++- src/lib/gamification/events.ts | 87 +++++-- src/lib/gamification/streaks.ts | 50 ++++- tests/unit/gamification/events.test.ts | 6 +- .../unit/gamification/streak-badge-xp.test.ts | 212 ++++++++++++++++++ 6 files changed, 353 insertions(+), 32 deletions(-) create mode 100644 changelog.d/features/12522-gamification-streak-badge-xp.md create mode 100644 tests/unit/gamification/streak-badge-xp.test.ts diff --git a/changelog.d/features/12522-gamification-streak-badge-xp.md b/changelog.d/features/12522-gamification-streak-badge-xp.md new file mode 100644 index 0000000000..d13d0ad942 --- /dev/null +++ b/changelog.d/features/12522-gamification-streak-badge-xp.md @@ -0,0 +1 @@ +- **feat(gamification): pay the documented `streak_bonus` and `badge_unlock` XP rewards.** `XP_REWARDS` listed both rewards but the award pipeline never paid them: the private reward table in `events.ts` omitted them, `updateStreak()` did not report when a streak extended, and badge unlocks carried no XP. Every request that extends a daily streak now pays `streak_bonus × streak length` once per UTC day (guarded by a same-day `xp_audit_log` check), and every badge unlocked through the pipeline pays `badge_unlock` once per badge (guarded by the `user_badges` primary key; `unlockBadge()` now reports whether it inserted). Bonus XP flows through the same `addXp` + level sync + global/weekly/monthly leaderboard path as action XP, so level-ups and rankings include it. The Radar supporter recognition unlock stays XP-free. (#12522 — thanks @pacocartones) diff --git a/src/lib/db/gamification.ts b/src/lib/db/gamification.ts index df452271a6..12085a633a 100644 --- a/src/lib/db/gamification.ts +++ b/src/lib/db/gamification.ts @@ -222,10 +222,17 @@ export function updateLevel(apiKeyId: string, level: number): void { // ──────────────── Badges ──────────────── -export function unlockBadge(apiKeyId: string, badgeId: string): void { - db() +/** + * Award a badge to an API key. Idempotent on the `(api_key_id, badge_id)` primary key. + * + * @returns `true` when this call inserted the badge, `false` when it was already earned. + * Callers that pay the `badge_unlock` XP reward key off this so a badge is paid once. + */ +export function unlockBadge(apiKeyId: string, badgeId: string): boolean { + const result = db() .prepare(`INSERT OR IGNORE INTO user_badges (api_key_id, badge_id) VALUES (?, ?)`) .run(apiKeyId, badgeId); + return result.changes > 0; } /** @@ -243,6 +250,24 @@ export function hasBadge(apiKeyId: string, badgeId: string): boolean { return !!row; } +/** + * Whether `xp_audit_log` already holds an entry for this action on the current UTC day. + * + * `created_at` is written by the table default `datetime('now')` as + * `"YYYY-MM-DD HH:MM:SS"` (UTC), so a lexical compare against `date('now')` selects + * today's rows. Used as the once-per-day guard for daily rewards such as `streak_bonus`. + */ +export function hasXpActionToday(apiKeyId: string, action: string): boolean { + const row = db() + .prepare( + `SELECT 1 FROM xp_audit_log + WHERE api_key_id = ? AND action = ? AND created_at >= date('now') + LIMIT 1` + ) + .get(apiKeyId, action); + return !!row; +} + export function getBadges(apiKeyId: string): UserBadge[] { const rows = db() .prepare( diff --git a/src/lib/gamification/events.ts b/src/lib/gamification/events.ts index 8c2ad62369..3560037a26 100644 --- a/src/lib/gamification/events.ts +++ b/src/lib/gamification/events.ts @@ -5,6 +5,7 @@ */ import { logger } from "../../../open-sse/utils/logger.ts"; +import { calculateLevel, XP_REWARDS } from "./xp"; const log = logger("GAMIFICATION"); @@ -57,23 +58,19 @@ export async function emitGamificationEvent(params: { const { addXp } = await import("../db/gamification"); addXp(apiKeyId, action, xpAmount, metadata ? JSON.stringify(metadata) : undefined); - // Update level - const { getXp, updateLevel } = await import("../db/gamification"); - const xp = getXp(apiKeyId); - if (xp) { - const { calculateLevel } = await import("./xp"); - const newLevel = calculateLevel(xp.totalXp); - if (newLevel !== xp.currentLevel) { - updateLevel(apiKeyId, newLevel); - log.info("events.level_up", { apiKeyId, oldLevel: xp.currentLevel, newLevel }); - } - } + await syncLevel(apiKeyId); } // 2. Update streak if (action === "request") { - const { updateStreak } = await import("./streaks"); - const streak = await updateStreak(apiKeyId); + const { advanceStreak } = await import("./streaks"); + const { currentStreak: streak, extended } = await advanceStreak(apiKeyId); + + // Pay the documented streak_bonus (XP_REWARDS: per consecutive streak day, multiplied + // by streak length) on the one request per UTC day that extends the streak. + if (extended) { + await awardStreakBonus(apiKeyId, streak); + } // Check streak badges if (streak >= 365) { @@ -112,6 +109,54 @@ export async function emitGamificationEvent(params: { } } +/** + * Recompute the level from total XP and persist it when it changed. + * Runs after every award so bonus XP (streaks, badges) also counts toward level-ups. + */ +async function syncLevel(apiKeyId: string): Promise { + const { getXp, updateLevel } = await import("../db/gamification"); + const xp = getXp(apiKeyId); + if (!xp) return; + const newLevel = calculateLevel(xp.totalXp); + if (newLevel !== xp.currentLevel) { + updateLevel(apiKeyId, newLevel); + log.info("events.level_up", { apiKeyId, oldLevel: xp.currentLevel, newLevel }); + } +} + +/** + * Award a bonus reward (`streak_bonus`, `badge_unlock`) through the same path as action XP: + * `xp_audit_log` + `user_levels` via addXp, level sync, and the global/weekly/monthly + * leaderboard scopes. Idempotency is the caller's responsibility. + */ +async function awardBonusXp( + apiKeyId: string, + action: "streak_bonus" | "badge_unlock", + amount: number, + metadata: Record +): Promise { + const { addXp } = await import("../db/gamification"); + addXp(apiKeyId, action, amount, JSON.stringify(metadata)); + await syncLevel(apiKeyId); + + const { updateScore } = await import("./leaderboard"); + await updateScore(apiKeyId, "global", amount); + await updateScore(apiKeyId, "weekly", amount); + await updateScore(apiKeyId, "monthly", amount); + log.info("events.bonus_awarded", { apiKeyId, action, amount, ...metadata }); +} + +/** + * Pay `streak_bonus × streak` once per UTC day. The `xp_audit_log` same-day check and the + * insert run synchronously with no await in between, so two requests racing at the day + * boundary cannot both pay. + */ +async function awardStreakBonus(apiKeyId: string, streak: number): Promise { + const { hasXpActionToday } = await import("../db/gamification"); + if (hasXpActionToday(apiKeyId, "streak_bonus")) return; + await awardBonusXp(apiKeyId, "streak_bonus", XP_REWARDS.streak_bonus * streak, { streak }); +} + /** * Get XP amount for an action. */ @@ -130,20 +175,28 @@ function getXpForAction(action: string): number { } /** - * Check and unlock a specific badge. + * Check and unlock a specific badge, paying the documented `badge_unlock` XP once per badge. + * + * @param rewardable - `false` for recognition-only unlocks (Radar supporter): the caller + * supplies a one-way identity, so the unlock neither earns XP nor logs the identity. */ async function checkAndUnlockBadge( apiKeyId: string, badgeId: string, - logIdentity = true + rewardable = true ): Promise { const { unlockBadge, hasBadge } = await import("../db/gamification"); // #3472: dedup via user_badges directly. getBadges() INNER-JOINs badge_definitions, which is // empty until seeded, so it falsely reported "not earned" and re-emitted the unlock event on // every request. if (!hasBadge(apiKeyId, badgeId)) { - unlockBadge(apiKeyId, badgeId); - log.info("events.badge_unlocked", logIdentity ? { apiKeyId, badgeId } : { badgeId }); + // unlockBadge is INSERT OR IGNORE on the (api_key_id, badge_id) primary key; only the call + // that actually inserts the row pays, so concurrent unlocks cannot double-pay. + const inserted = unlockBadge(apiKeyId, badgeId); + log.info("events.badge_unlocked", rewardable ? { apiKeyId, badgeId } : { badgeId }); + if (inserted && rewardable) { + await awardBonusXp(apiKeyId, "badge_unlock", XP_REWARDS.badge_unlock, { badgeId }); + } // Look up badge details from badge_definitions const { getDbInstance } = await import("../db/core"); diff --git a/src/lib/gamification/streaks.ts b/src/lib/gamification/streaks.ts index 4406375ac1..9c9303375c 100644 --- a/src/lib/gamification/streaks.ts +++ b/src/lib/gamification/streaks.ts @@ -157,7 +157,39 @@ export async function getAggregateStreak(): Promise< * console.log(count); // 8 */ export async function updateStreak(apiKeyId: string): Promise { - if (isBuildPhase || isCloud) return 0; + const { currentStreak } = await advanceStreak(apiKeyId); + return currentStreak; +} + +/** + * Result of {@link advanceStreak}. + */ +export interface StreakAdvance { + /** Current consecutive active days after this call */ + currentStreak: number; + /** + * `true` only on the call that extended the streak onto a new consecutive day + * (yesterday was active, today was not yet counted). `false` when today was + * already counted, when a new streak starts at 1, or when streaks are disabled. + */ + extended: boolean; +} + +/** + * Same as {@link updateStreak}, but also reports whether this call extended the + * streak onto a new consecutive day. The award pipeline uses `extended` to pay + * the `streak_bonus` reward once per UTC day; repeated requests on the same day + * see `extended: false` because the record already carries today's date. + * + * @param apiKeyId - The API key identifier + * @returns The new streak count and whether it just extended + * + * @example + * const { currentStreak, extended } = await advanceStreak("key_abc123"); + * if (extended) console.log(`day ${currentStreak} of the streak`); + */ +export async function advanceStreak(apiKeyId: string): Promise { + if (isBuildPhase || isCloud) return { currentStreak: 0, extended: false }; const db = getDbInstance() as unknown as DbLike; const today = todayUtc(); @@ -165,19 +197,13 @@ export async function updateStreak(apiKeyId: string): Promise { // Already counted today if (streak.lastActiveDate === today) { - return streak.currentStreak; + return { currentStreak: streak.currentStreak, extended: false }; } const yesterday = yesterdayUtc(); - let newStreak: number; - - if (streak.lastActiveDate === yesterday) { - // Consecutive day — extend streak - newStreak = streak.currentStreak + 1; - } else { - // Streak broken or first activity — start fresh - newStreak = 1; - } + const extended = streak.lastActiveDate === yesterday; + // Consecutive day — extend streak; otherwise streak broken or first activity — start fresh + const newStreak = extended ? streak.currentStreak + 1 : 1; const newData: StreakData = { currentStreak: newStreak, @@ -192,5 +218,5 @@ export async function updateStreak(apiKeyId: string): Promise { JSON.stringify(newData) ); - return newStreak; + return { currentStreak: newStreak, extended }; } diff --git a/tests/unit/gamification/events.test.ts b/tests/unit/gamification/events.test.ts index 0e2a9b6ed3..41a21b474e 100644 --- a/tests/unit/gamification/events.test.ts +++ b/tests/unit/gamification/events.test.ts @@ -1,6 +1,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { emitGamificationEvent } from "../../../src/lib/gamification/events"; +import { XP_REWARDS } from "../../../src/lib/gamification/xp"; import { getDbInstance } from "../../../src/lib/db/core"; describe("Gamification Events", () => { @@ -107,7 +108,10 @@ describe("Gamification Events", () => { await emitGamificationEvent({ apiKeyId: key, action: "request" }); assert.equal(countRequestRows(key), 1); - assert.equal(leaderboardScore(key), 1); + // The very first request also unlocks the "first-token" badge, and badge unlocks now + // pay XP_REWARDS.badge_unlock through the same leaderboard path. The gate only governs + // the action award, so the score is the 1 XP action plus the badge bonus. + assert.equal(leaderboardScore(key), 1 + XP_REWARDS.badge_unlock); cleanup(key); }); diff --git a/tests/unit/gamification/streak-badge-xp.test.ts b/tests/unit/gamification/streak-badge-xp.test.ts new file mode 100644 index 0000000000..21eea0815d --- /dev/null +++ b/tests/unit/gamification/streak-badge-xp.test.ts @@ -0,0 +1,212 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { emitGamificationEvent } from "../../../src/lib/gamification/events"; +import { advanceStreak, getStreak } from "../../../src/lib/gamification/streaks"; +import { XP_REWARDS } from "../../../src/lib/gamification/xp"; +import { addXp, getXp, unlockBadge } from "../../../src/lib/db/gamification"; +import { getDbInstance } from "../../../src/lib/db/core"; + +// `XP_REWARDS` documents `streak_bonus` ("per consecutive streak day, multiplied by streak +// length") and `badge_unlock`, but the award pipeline never paid either: events.ts kept a +// private reward table without them, updateStreak() did not report whether the streak had +// just extended, and checkAndUnlockBadge() unlocked badges without XP. These tests pin the +// documented rewards and their idempotency guards (once per UTC day, once per badge). + +const MS_PER_DAY = 86_400_000; +const STREAK_NS = "gamification:streaks"; + +function utcDate(offsetDays: number): string { + return new Date(Date.now() - offsetDays * MS_PER_DAY).toISOString().split("T")[0]; +} + +function seedStreak(apiKeyId: string, currentStreak: number, lastActiveDaysAgo: number): void { + const lastActiveDate = utcDate(lastActiveDaysAgo); + getDbInstance() + .prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)") + .run( + STREAK_NS, + apiKeyId, + JSON.stringify({ + currentStreak, + longestStreak: currentStreak, + lastActiveDate, + streakStartDate: utcDate(lastActiveDaysAgo + currentStreak - 1), + }) + ); +} + +function auditRows( + apiKeyId: string, + action: string +): Array<{ xp_earned: number; metadata: string | null }> { + return getDbInstance() + .prepare("SELECT xp_earned, metadata FROM xp_audit_log WHERE api_key_id = ? AND action = ?") + .all(apiKeyId, action) as Array<{ xp_earned: number; metadata: string | null }>; +} + +function auditTotal(apiKeyId: string): number { + const row = getDbInstance() + .prepare("SELECT COALESCE(SUM(xp_earned), 0) AS total FROM xp_audit_log WHERE api_key_id = ?") + .get(apiKeyId) as { total: number }; + return row.total; +} + +function leaderboardScore(apiKeyId: string, scope: string): number { + const row = getDbInstance() + .prepare("SELECT score FROM leaderboard WHERE api_key_id = ? AND scope = ?") + .get(apiKeyId, scope) as { score: number } | undefined; + return row?.score ?? 0; +} + +function cleanup(apiKeyId: string): void { + const db = getDbInstance(); + db.prepare("DELETE FROM xp_audit_log WHERE api_key_id = ?").run(apiKeyId); + db.prepare("DELETE FROM user_levels WHERE api_key_id = ?").run(apiKeyId); + db.prepare("DELETE FROM user_badges WHERE api_key_id = ?").run(apiKeyId); + db.prepare("DELETE FROM leaderboard WHERE api_key_id = ?").run(apiKeyId); + db.prepare("DELETE FROM key_value WHERE namespace = ? AND key = ?").run(STREAK_NS, apiKeyId); +} + +describe("streak bonus XP", () => { + it("advanceStreak reports whether the streak extended today", async () => { + const key = `sb-advance-${Date.now()}`; + try { + seedStreak(key, 1, 1); + const first = await advanceStreak(key); + assert.deepEqual(first, { currentStreak: 2, extended: true }); + const second = await advanceStreak(key); + assert.deepEqual(second, { currentStreak: 2, extended: false }, "same day is a no-op"); + } finally { + cleanup(key); + } + }); + + it("pays streak_bonus x streak length on the day the streak extends", async () => { + const key = `sb-pay-${Date.now()}`; + try { + seedStreak(key, 1, 1); // active yesterday → today's request extends to 2 + await emitGamificationEvent({ apiKeyId: key, action: "request" }); + + const rows = auditRows(key, "streak_bonus"); + assert.equal(rows.length, 1, "exactly one streak_bonus audit row"); + assert.equal(rows[0].xp_earned, XP_REWARDS.streak_bonus * 2); + assert.deepEqual(JSON.parse(rows[0].metadata ?? "{}"), { streak: 2 }); + assert.equal((await getStreak(key)).currentStreak, 2); + + const total = auditTotal(key); + assert.equal(getXp(key)?.totalXp, total, "user_levels.total_xp matches the audit log"); + assert.equal(leaderboardScore(key, "global"), total, "global leaderboard credits the bonus"); + assert.equal(leaderboardScore(key, "weekly"), total); + assert.equal(leaderboardScore(key, "monthly"), total); + } finally { + cleanup(key); + } + }); + + it("pays the bonus once per UTC day even when requests repeat", async () => { + const key = `sb-once-${Date.now()}`; + try { + seedStreak(key, 4, 1); + await emitGamificationEvent({ apiKeyId: key, action: "request" }); + await emitGamificationEvent({ apiKeyId: key, action: "request" }); + await emitGamificationEvent({ apiKeyId: key, action: "request" }); + + const rows = auditRows(key, "streak_bonus"); + assert.equal(rows.length, 1); + assert.equal(rows[0].xp_earned, XP_REWARDS.streak_bonus * 5); + } finally { + cleanup(key); + } + }); + + it("does not pay on the first day of a streak or after a broken streak", async () => { + const fresh = `sb-fresh-${Date.now()}`; + const broken = `sb-broken-${Date.now()}`; + try { + await emitGamificationEvent({ apiKeyId: fresh, action: "request" }); + assert.equal(auditRows(fresh, "streak_bonus").length, 0, "day 1 is not a consecutive day"); + + seedStreak(broken, 6, 3); // last active three days ago → streak resets to 1 + await emitGamificationEvent({ apiKeyId: broken, action: "request" }); + assert.equal((await getStreak(broken)).currentStreak, 1); + assert.equal(auditRows(broken, "streak_bonus").length, 0); + } finally { + cleanup(fresh); + cleanup(broken); + } + }); +}); + +describe("badge unlock XP", () => { + it("unlockBadge reports whether a new row was inserted", () => { + const key = `bu-insert-${Date.now()}`; + try { + assert.equal(unlockBadge(key, "first-token"), true); + assert.equal(unlockBadge(key, "first-token"), false, "INSERT OR IGNORE → no new row"); + } finally { + cleanup(key); + } + }); + + it("pays badge_unlock once per badge when the pipeline unlocks it", async () => { + const key = `bu-pay-${Date.now()}`; + try { + await emitGamificationEvent({ apiKeyId: key, action: "request" }); // → first-token + await emitGamificationEvent({ apiKeyId: key, action: "request" }); // already earned + + const rows = auditRows(key, "badge_unlock"); + assert.equal(rows.length, 1, "exactly one badge_unlock audit row"); + assert.equal(rows[0].xp_earned, XP_REWARDS.badge_unlock); + assert.deepEqual(JSON.parse(rows[0].metadata ?? "{}"), { badgeId: "first-token" }); + + const total = auditTotal(key); + assert.equal(total, 2 * XP_REWARDS.request + XP_REWARDS.badge_unlock); + assert.equal(getXp(key)?.totalXp, total); + assert.equal(leaderboardScore(key, "global"), total); + } finally { + cleanup(key); + } + }); + + it("pays the streak badge and the streak bonus from the same request", async () => { + const key = `bu-streak-${Date.now()}`; + try { + seedStreak(key, 2, 1); // → 3 today: daily-user badge + bonus + await emitGamificationEvent({ apiKeyId: key, action: "request" }); + + const badgeRows = auditRows(key, "badge_unlock"); + const unlocked = badgeRows.map((r) => JSON.parse(r.metadata ?? "{}").badgeId).sort(); + assert.deepEqual(unlocked, ["daily-user", "first-token"]); + assert.equal(auditRows(key, "streak_bonus")[0]?.xp_earned, XP_REWARDS.streak_bonus * 3); + } finally { + cleanup(key); + } + }); + + it("recomputes the level after bonus XP, not only after the action XP", async () => { + const key = `bu-level-${Date.now()}`; + try { + // Level 2 needs 282 XP. 280 + 1 (request) = 281 stays level 1; the first-token + // badge_unlock XP crosses the threshold, so the level must be synced after it. + addXp(key, "request", 280); + assert.equal(getXp(key)?.currentLevel, 1); + await emitGamificationEvent({ apiKeyId: key, action: "request" }); + assert.equal(getXp(key)?.totalXp, 280 + XP_REWARDS.request + XP_REWARDS.badge_unlock); + assert.equal(getXp(key)?.currentLevel, 2); + } finally { + cleanup(key); + } + }); + + it("keeps the radar_supporter recognition path free of XP", async () => { + const identity = `bu-radar-${Date.now()}`; + try { + await emitGamificationEvent({ apiKeyId: identity, action: "radar_supporter" }); + assert.equal(auditRows(identity, "badge_unlock").length, 0); + assert.equal(getXp(identity), null); + assert.equal(leaderboardScore(identity, "global"), 0); + } finally { + cleanup(identity); + } + }); +}); From 3f62e4369656b66913b2c72767597871fe2d4cb1 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 11 Sep 2026 17:46:22 -0300 Subject: [PATCH 18/20] test(compression): assert idle eviction terminates at the resource level (#13371) Merged as the credit vehicle for #12542. Reverse-TDD verified on the tip: 8/8 with the fix, 7/8 with `terminate()` disabled. --- .../12542-compression-idle-terminate-test.md | 1 + .../compression/compression-worker.test.ts | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 changelog.d/maintenance/12542-compression-idle-terminate-test.md diff --git a/changelog.d/maintenance/12542-compression-idle-terminate-test.md b/changelog.d/maintenance/12542-compression-idle-terminate-test.md new file mode 100644 index 0000000000..744e38f854 --- /dev/null +++ b/changelog.d/maintenance/12542-compression-idle-terminate-test.md @@ -0,0 +1 @@ +- **test(compression):** cover idle worker eviction at the resource level — the pool must call `terminate()` and must not retain the worker's `MessagePort`, complementing the `exit`-event assertion added with the fix diff --git a/tests/unit/compression/compression-worker.test.ts b/tests/unit/compression/compression-worker.test.ts index 0ca4cbd453..93265c91e7 100644 --- a/tests/unit/compression/compression-worker.test.ts +++ b/tests/unit/compression/compression-worker.test.ts @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { after, describe, it } from "node:test"; +import { Worker } from "node:worker_threads"; import { isCompressionWorkerEligible, isStrictlySerializable, @@ -136,6 +137,40 @@ describe("compression worker execution", () => { } }); + it("terminates an idle worker instead of only dropping it from the pool", async () => { + const spawned = new Set(); + const terminated: Promise[] = []; + const originalPostMessage = Worker.prototype.postMessage; + const originalTerminate = Worker.prototype.terminate; + Worker.prototype.postMessage = function (this: Worker, ...args) { + spawned.add(this); + return originalPostMessage.apply(this, args); + }; + Worker.prototype.terminate = function (this: Worker) { + const exit = originalTerminate.call(this); + terminated.push(exit); + return exit; + }; + const messagePorts = () => + process.getActiveResourcesInfo().filter((resource) => resource === "MessagePort").length; + const portsBefore = messagePorts(); + const pool = new CompressionWorkerPool({ size: 1, idleMs: 50 }); + try { + await pool.run(body, "stacked", { config }); + await new Promise((resolve) => setTimeout(resolve, 300)); + assert.equal(spawned.size, 1); + assert.equal(terminated.length, 1, "idle eviction must terminate the worker thread"); + await Promise.all(terminated); + assert.ok(messagePorts() <= portsBefore, "idle eviction must not retain the worker's port"); + } finally { + Worker.prototype.postMessage = originalPostMessage; + Worker.prototype.terminate = originalTerminate; + await pool.close(); + // Reap anything the pool forgot so a regression fails instead of hanging the runner. + await Promise.all([...spawned].map((worker) => worker.terminate().catch(() => undefined))); + } + }); + it("keeps the parent event loop responsive while two workers overlap", async () => { const largeBody = { messages: Array.from({ length: 400 }, (_, index) => ({ From 374bbe3ee7aaa8052d8ebd6829b31a72bac50312 Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:09:58 +0200 Subject: [PATCH 19/20] fix(i18n): translate the home Recent Requests panel and topology legend (#12551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The diagnosis is the valuable part here: a verbatim English copy is invisible to every i18n gate — `check-ui-keys-coverage` counts it as covered, `sync-ui-keys` only backfills absent keys, and `check-ui-value-drift` only reacts to English values that change. That is exactly how five keys shipped in #10900 stayed English in 39 catalogs without anything noticing, and a static test asserting "not equal to the English value" is the right instrument for it. Two things needed reconciling before merge, both pure drift from the time this sat open: **Nine locales did not exist when you cut this branch** — el, et, ga, hr, lt, lv, mt, sl, sr. They arrived with the Recent Requests keys but not the topology legend ones, so your own first assertion failed on them. I filled the three keys from each catalog's existing approved translations of the same words (`common.active`, `common.recent`, `analytics.modelStatusError`) rather than a fresh translation pass, so the legend reads the same as the rest of that language's dashboard. **Two cognates were being failed for being correct** — `hr.recentRequestsModel` and `sl.recentRequestsModel` are "Model", which is the right word in Croatian and Slovenian. Your `COGNATES` set already existed for exactly this (`es.topologyLegendError`), but the third assertion swept every locale without consulting it. It does now. 5/5 on the suite afterwards, and the diff stayed at the nine locale files plus the test — no collateral sync. --- Validated in the consolidated worktree for this batch. `typecheck:core` clean, `check:dashboard-typecheck` OK, complexity and cognitive-complexity under baseline. ⚠️ base-red inherited: #12732 — and separately, `npm run i18n:check` reports 75 doc-translation drift entries across 35 files on the pure tip (40 of them `docs/reference/ENVIRONMENT.md`). That is the docs pipeline, untouched by this PR. Thanks @pacocartones — 17 more of yours merged today. --- ...2551-i18n-home-recent-requests-topology.md | 1 + .../dashboard/HomeProviderTopologySection.tsx | 9 +- src/i18n/messages/ar.json | 13 +- src/i18n/messages/az.json | 13 +- src/i18n/messages/bg.json | 13 +- src/i18n/messages/bn.json | 13 +- src/i18n/messages/cs.json | 13 +- src/i18n/messages/da.json | 13 +- src/i18n/messages/de.json | 13 +- src/i18n/messages/el.json | 3 + src/i18n/messages/en.json | 3 + src/i18n/messages/es.json | 13 +- src/i18n/messages/et.json | 3 + src/i18n/messages/fa.json | 13 +- src/i18n/messages/fi.json | 13 +- src/i18n/messages/fr.json | 13 +- src/i18n/messages/ga.json | 3 + src/i18n/messages/gu.json | 13 +- src/i18n/messages/he.json | 13 +- src/i18n/messages/hi.json | 13 +- src/i18n/messages/hr.json | 3 + src/i18n/messages/hu.json | 13 +- src/i18n/messages/id.json | 13 +- src/i18n/messages/it.json | 13 +- src/i18n/messages/ja.json | 13 +- src/i18n/messages/ko.json | 13 +- src/i18n/messages/lt.json | 3 + src/i18n/messages/lv.json | 3 + src/i18n/messages/mr.json | 13 +- src/i18n/messages/ms.json | 13 +- src/i18n/messages/mt.json | 3 + src/i18n/messages/nl.json | 13 +- src/i18n/messages/no.json | 13 +- src/i18n/messages/phi.json | 13 +- src/i18n/messages/pl.json | 13 +- src/i18n/messages/pt-BR.json | 3 + src/i18n/messages/pt.json | 13 +- src/i18n/messages/ro.json | 13 +- src/i18n/messages/ru.json | 13 +- src/i18n/messages/sk.json | 13 +- src/i18n/messages/sl.json | 3 + src/i18n/messages/sr.json | 3 + src/i18n/messages/sv.json | 13 +- src/i18n/messages/sw.json | 13 +- src/i18n/messages/ta.json | 13 +- src/i18n/messages/te.json | 13 +- src/i18n/messages/th.json | 13 +- src/i18n/messages/tr.json | 13 +- src/i18n/messages/uk-UA.json | 13 +- src/i18n/messages/ur.json | 13 +- src/i18n/messages/vi.json | 3 + src/i18n/messages/zh-CN.json | 13 +- src/i18n/messages/zh-TW.json | 13 +- ...me-recent-requests-topology-legend.test.ts | 134 ++++++++++++++++++ 54 files changed, 486 insertions(+), 201 deletions(-) create mode 100644 changelog.d/fixes/12551-i18n-home-recent-requests-topology.md create mode 100644 tests/unit/i18n-home-recent-requests-topology-legend.test.ts diff --git a/changelog.d/fixes/12551-i18n-home-recent-requests-topology.md b/changelog.d/fixes/12551-i18n-home-recent-requests-topology.md new file mode 100644 index 0000000000..87dbbcf0b7 --- /dev/null +++ b/changelog.d/fixes/12551-i18n-home-recent-requests-topology.md @@ -0,0 +1 @@ +- **fix(i18n):** the home "Recent Requests" panel and the Provider Topology legend are now translated instead of rendering English copies on non-English dashboards; the legend reads its own `home.topologyLegend*` labels with consistent casing rather than borrowing the memory-settings "Recent" and analytics "Error" strings (#12551 — thanks @pacocartones). diff --git a/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx b/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx index 034088e7b8..172cb5a1dc 100644 --- a/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx +++ b/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx @@ -29,9 +29,6 @@ export function HomeProviderTopologySection({ enabled?: boolean; }) { const t = useTranslations("home"); - const tCommon = useTranslations("common"); - const tSettings = useTranslations("settings"); - const tAnalytics = useTranslations("analytics"); // #4596: gate the live-WS connection so it only opens while the topology // section is actually shown on the home page. const { activeRequests: liveActiveRequests } = useLiveRequests({ enabled }); @@ -50,15 +47,15 @@ export function HomeProviderTopologySection({
- {tCommon("active")} + {t("topologyLegendActive")} - {tSettings("recent")} + {t("topologyLegendRecent")} - {tAnalytics("modelStatusError")} + {t("topologyLegendError")}
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index bc3296b89a..8727f7c644 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1807,6 +1807,9 @@ "healthMonitor": "مراقب الصحة", "reportIssue": "الإبلاغ عن مشكلة", "activeError": "{active} نشط · {errors} خطأ", + "topologyLegendActive": "نشط", + "topologyLegendRecent": "الأحدث", + "topologyLegendError": "خطأ", "oauthLabel": "OAuth", "apiKeyLabel": "مفتاح واجهة برمجة التطبيقات", "requestsShort": "{count} طلب", @@ -1819,11 +1822,11 @@ "updateStarted": "بدأ التحديث...", "reloadingPageAutomatically": "جارٍ إعادة تحميل الصفحة تلقائيًا...", "providerTopology": "طوبولوجيا الموفر", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "تحميل DMG (macOS)", "downloadDmgDescription": "يتوفر إصدار جديد من تطبيق OmniRoute لسطح المكتب. يرجى تنزيل وتثبيت مثبت DMG لنظام macOS للتحديث (الحالي: v{version}).", "downloadExe": "تحميل EXE (ويندوز)", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index faca0d6278..e9f01e9101 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "Səhifə avtomatik yenidən yüklənir...", "providerTopology": "Provayder Topologiyası", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG-ni Yükləyin (macOS)", "downloadDmgDescription": "OmniRoute masaüstü tətbiqinin yeni versiyası mövcuddur. Zəhmət olmasa, yeniləmək üçün macOS DMG quraşdırıcısını yükləyin və quraşdırın (hazırkı: v{version}).", "downloadExe": "EXE-ni Yükləyin (Windows)", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index ef8c3023ff..1ed0ad3156 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Здравен монитор", "reportIssue": "Докладвайте за проблем", "activeError": "{active} активен · {errors} грешка", + "topologyLegendActive": "Активен", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Грешка", "oauthLabel": "OAuth", "apiKeyLabel": "API ключ", "requestsShort": "{count} изискване", @@ -1819,11 +1822,11 @@ "updateStarted": "Актуализацията започна...", "reloadingPageAutomatically": "Страницата се презарежда автоматично...", "providerTopology": "Топология на доставчика", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Изтеглете DMG (macOS)", "downloadDmgDescription": "Налична е нова версия на настолната апликация OmniRoute. Моля, изтеглете и инсталирайте DMG инсталатора за macOS, за да актуализирате (текуща: v{version}).", "downloadExe": "Изтеглете EXE (Windows)", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 5906b9e495..1fe279b876 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "স্বয়ংক্রিয়ভাবে পৃষ্ঠা পুনরায় লোড হচ্ছে...", "providerTopology": "প্রদানকারী টপোলজি", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG ডাউনলোড করুন (macOS)", "downloadDmgDescription": "OmniRoute ডেস্কটপ অ্যাপের একটি নতুন সংস্করণ উপলব্ধ। আপডেট করতে দয়া করে macOS DMG ইনস্টলার ডাউনলোড এবং ইনস্টল করুন (বর্তমান: v{version})।", "downloadExe": "EXE ডাউনলোড করুন (Windows)", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 1290e1e172..e5eace9595 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Monitor stavu", "reportIssue": "Nahlásit problém", "activeError": "{active} aktivní · {errors} chyba", + "topologyLegendActive": "Aktivní", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Chyba", "oauthLabel": "OAuth", "apiKeyLabel": "API Klíč", "requestsShort": "{count} požadavků", @@ -1819,11 +1822,11 @@ "updateStarted": "Aktualizace začala...", "reloadingPageAutomatically": "Automatické opětovné načítání stránky...", "providerTopology": "Topologie poskytovatele", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Stáhnout DMG (macOS)", "downloadDmgDescription": "Nová verze desktopové aplikace OmniRoute je k dispozici. Prosím, stáhněte a nainstalujte macOS DMG instalátor pro aktualizaci (aktuální: v{version}).", "downloadExe": "Stáhnout EXE (Windows)", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index d65eb7d96b..3c5d0ea371 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Sundhedsmonitor", "reportIssue": "Rapportér problem", "activeError": "{active} aktiv · {errors} fejl", + "topologyLegendActive": "Aktiv", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Fejl", "oauthLabel": "OAuth", "apiKeyLabel": "API nøgle", "requestsShort": "{count} req", @@ -1819,11 +1822,11 @@ "updateStarted": "Opdatering startet...", "reloadingPageAutomatically": "Genindlæser siden automatisk...", "providerTopology": "Udbydertopologi", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Download DMG (macOS)", "downloadDmgDescription": "En ny version af OmniRoute desktopappen er tilgængelig. Download og installer venligst macOS DMG-installationsprogrammet for at opdatere (nuværende: v{version}).", "downloadExe": "Download EXE (Windows)", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 9fd735d513..fdd781ec5b 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Gesundheitsmonitor", "reportIssue": "Problem melden", "activeError": "{active} aktiv · {errors} Fehler", + "topologyLegendActive": "Aktiv", + "topologyLegendRecent": "Zuletzt", + "topologyLegendError": "Fehler", "oauthLabel": "OAuth", "apiKeyLabel": "API-Schlüssel", "requestsShort": "{count} Anfr.", @@ -1819,11 +1822,11 @@ "updateStarted": "Aktualisierung gestartet...", "reloadingPageAutomatically": "Seite wird automatisch neu geladen...", "providerTopology": "Anbietertopologie", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "Letzte Anfragen", + "recentRequestsEmpty": "Noch keine Anfragen.", + "recentRequestsModel": "Modell", + "recentRequestsTokens": "Eingabe / Ausgabe", + "recentRequestsWhen": "Wann", "downloadDmg": "DMG herunterladen (macOS)", "downloadDmgDescription": "Eine neue Version der OmniRoute-Desktop-App ist verfügbar. Bitte laden Sie den macOS DMG-Installer herunter und installieren Sie ihn, um zu aktualisieren (aktuell: v{version}).", "downloadExe": "EXE herunterladen (Windows)", diff --git a/src/i18n/messages/el.json b/src/i18n/messages/el.json index c3abae12ee..0293177e6d 100644 --- a/src/i18n/messages/el.json +++ b/src/i18n/messages/el.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Μοντέλο", "recentRequestsTokens": "Είσοδος / Έξοδος", "recentRequestsWhen": "Πότε", + "topologyLegendActive": "Ενεργό", + "topologyLegendRecent": "Πρόσφατα", + "topologyLegendError": "Σφάλμα", "downloadDmg": "Λήψη DMG (macOS)", "downloadDmgDescription": "Διατίθεται νέα έκδοση της εφαρμογής OmniRoute για επιτραπέζιους υπολογιστές. Παρακαλούμε κατεβάστε και εγκαταστήστε το πρόγραμμα εγκατάστασης DMG για macOS για να ενημερωθείτε (τρέχουσα: v{version}).", "downloadExe": "Λήψη EXE (Windows)", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index ab6a8347bb..aef7ee8521 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "Active", + "topologyLegendRecent": "Recent", + "topologyLegendError": "Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 916769ea85..74b24e0612 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Monitor de salud", "reportIssue": "Informar problema", "activeError": "{active} activo · {errors} error", + "topologyLegendActive": "Activo", + "topologyLegendRecent": "Reciente", + "topologyLegendError": "Error", "oauthLabel": "OAuth", "apiKeyLabel": "Clave API", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Actualización iniciada...", "reloadingPageAutomatically": "Recargando página automáticamente...", "providerTopology": "Topología del proveedor", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "Solicitudes recientes", + "recentRequestsEmpty": "Aún no hay solicitudes.", + "recentRequestsModel": "Modelo", + "recentRequestsTokens": "Entrada / Salida", + "recentRequestsWhen": "Cuándo", "downloadDmg": "Descargar DMG (macOS)", "downloadDmgDescription": "Una nueva versión de la aplicación de escritorio OmniRoute está disponible. Por favor, descarga e instala el instalador DMG de macOS para actualizar (actual: v{version}).", "downloadExe": "Descargar EXE (Windows)", diff --git a/src/i18n/messages/et.json b/src/i18n/messages/et.json index 0fce44c58b..0d594bc934 100644 --- a/src/i18n/messages/et.json +++ b/src/i18n/messages/et.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Mudel", "recentRequestsTokens": "Sisend / väljund", "recentRequestsWhen": "Millal", + "topologyLegendActive": "Aktiivne", + "topologyLegendRecent": "Hiljutine", + "topologyLegendError": "Viga", "downloadDmg": "Laadi alla DMG (macOS)", "downloadDmgDescription": "Saadaval on OmniRoute’i töölauarakenduse uus versioon. Värskendamiseks laadige alla ja installige macOS-i DMG-paigaldusprogramm (praegune: v{version}).", "downloadExe": "Laadi alla EXE (Windows)", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 93897b3025..72143731a5 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "بارگیری مجدد صفحه به صورت خودکار...", "providerTopology": "توپولوژی ارائه دهنده", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "دانلود DMG (macOS)", "downloadDmgDescription": "نسخه جدیدی از برنامه دسکتاپ OmniRoute در دسترس است. لطفاً DMG نصب‌کننده macOS را دانلود و نصب کنید تا به‌روزرسانی کنید (فعلی: v{version}).", "downloadExe": "دانلود EXE (ویندوز)", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index 16f74b5129..904286fe8b 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Terveysmittari", "reportIssue": "Ilmoita ongelmasta", "activeError": "{active} aktiivinen · {errors} virhe", + "topologyLegendActive": "Aktiivinen", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Virhe", "oauthLabel": "OAuth", "apiKeyLabel": "API-avain", "requestsShort": "{count} vaatimus", @@ -1819,11 +1822,11 @@ "updateStarted": "Päivitys aloitettu...", "reloadingPageAutomatically": "Ladataan sivua automaattisesti uudelleen...", "providerTopology": "Palveluntarjoajan topologia", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Lataa DMG (macOS)", "downloadDmgDescription": "Uusi versio OmniRoute-työpöytäsovelluksesta on saatavilla. Lataa ja asenna macOS DMG -asennustiedosto päivittääksesi (nykyinen: v{version}).", "downloadExe": "Lataa EXE (Windows)", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index ac177d9460..ac9844ee43 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Moniteur de santé", "reportIssue": "Signaler un problème", "activeError": "{active} actif · Erreur {errors}", + "topologyLegendActive": "Actif", + "topologyLegendRecent": "Récent", + "topologyLegendError": "Erreur", "oauthLabel": "OAuth", "apiKeyLabel": "Clé API", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Mise à jour démarrée...", "reloadingPageAutomatically": "Rechargement automatique de la page...", "providerTopology": "Topologie du fournisseur", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "Requêtes récentes", + "recentRequestsEmpty": "Aucune requête pour le moment.", + "recentRequestsModel": "Modèle", + "recentRequestsTokens": "Entrée / Sortie", + "recentRequestsWhen": "Quand", "downloadDmg": "Télécharger le DMG (macOS)", "downloadDmgDescription": "Une nouvelle version de l'application de bureau OmniRoute est disponible. Téléchargez et installez le programme d'installation DMG macOS pour effectuer la mise à jour (version actuelle : v{version}).", "downloadExe": "Télécharger l'EXE (Windows)", diff --git a/src/i18n/messages/ga.json b/src/i18n/messages/ga.json index 0edc4d5eec..55201fab8b 100644 --- a/src/i18n/messages/ga.json +++ b/src/i18n/messages/ga.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Samhail", "recentRequestsTokens": "Isteach / Amach", "recentRequestsWhen": "Cathain", + "topologyLegendActive": "Gníomhach", + "topologyLegendRecent": "Le déanaí", + "topologyLegendError": "Earráid", "downloadDmg": "Íoslódáil DMG (macOS)", "downloadDmgDescription": "Tá leagan nua den fheidhmchlár deisce OmniRoute ar fáil. Íoslódáil agus suiteáil an suiteálaí DMG macOS le nuashonrú (reatha: v{version}).", "downloadExe": "Íoslódáil EXE (Windows)", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 6ae331a26b..4661615232 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "પૃષ્ઠને આપમેળે ફરીથી લોડ કરી રહ્યું છે...", "providerTopology": "પ્રદાતા ટોપોલોજી", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG ડાઉનલોડ કરો (macOS)", "downloadDmgDescription": "ઓમ્નીરૂટ ડેસ્કટોપ એપ્લિકેશનનો નવો સંસ્કરણ ઉપલબ્ધ છે. કૃપા કરીને અપડેટ કરવા માટે macOS DMG ઇન્સ્ટોલર ડાઉનલોડ અને ઇન્સ્ટોલ કરો (વર્તમાન: v{version}).", "downloadExe": "ડાઉનલોડ EXE (Windows)", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index c43184a1df..c56f707366 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -1807,6 +1807,9 @@ "healthMonitor": "מוניטור בריאות", "reportIssue": "דווח על בעיה", "activeError": "{active} פעיל · שגיאה {errors}", + "topologyLegendActive": "פעיל", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "שגיאה", "oauthLabel": "OAuth", "apiKeyLabel": "מפתח API", "requestsShort": "{count} בקשות", @@ -1819,11 +1822,11 @@ "updateStarted": "העדכון התחיל...", "reloadingPageAutomatically": "טוען מחדש את הדף באופן אוטומטי...", "providerTopology": "טופולוגיה של ספק", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "הורד DMG (macOS)", "downloadDmgDescription": "גרסה חדשה של אפליקציית OmniRoute למחשב שולחני זמינה. אנא הורד והתקן את מתקין ה-DMG של macOS כדי לעדכן (נוכחי: v{version}).", "downloadExe": "הורד EXE (Windows)", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index df7c596dbc..bf45caf5ef 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -1807,6 +1807,9 @@ "healthMonitor": "स्वास्थ्य मॉनिटर", "reportIssue": "रिपोर्ट मुद्दा", "activeError": "{active} सक्रिय · {errors} त्रुटि", + "topologyLegendActive": "सक्रिय", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "त्रुटि", "oauthLabel": "OAuth", "apiKeyLabel": "एपीआई कुंजी", "requestsShort": "{count} अनुरोध", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "पृष्ठ स्वचालित रूप से पुनः लोड हो रहा है...", "providerTopology": "प्रदाता टोपोलॉजी", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG डाउनलोड करें (macOS)", "downloadDmgDescription": "OmniRoute डेस्कटॉप ऐप का एक नया संस्करण उपलब्ध है। कृपया अपडेट करने के लिए macOS DMG इंस्टॉलर डाउनलोड और इंस्टॉल करें (वर्तमान: v{version})।", "downloadExe": "EXE डाउनलोड करें (Windows)", diff --git a/src/i18n/messages/hr.json b/src/i18n/messages/hr.json index df1de35a5d..2a2a27864c 100644 --- a/src/i18n/messages/hr.json +++ b/src/i18n/messages/hr.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Model", "recentRequestsTokens": "Ulaz / Izlaz", "recentRequestsWhen": "Kada", + "topologyLegendActive": "Aktivno", + "topologyLegendRecent": "Nedavno", + "topologyLegendError": "Greška", "downloadDmg": "Preuzmi DMG (macOS)", "downloadDmgDescription": "Dostupna je nova verzija OmniRoute desktop aplikacije. Preuzmite i instalirajte macOS DMG instalacijski paket za ažuriranje (trenutna verzija: v{version}).", "downloadExe": "Preuzmi EXE (Windows)", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index d199204fd1..3bed874d82 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Egészségügyi Monitor", "reportIssue": "Probléma bejelentése", "activeError": "{active} aktív · {errors} hiba", + "topologyLegendActive": "Aktív", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Hiba", "oauthLabel": "OAuth", "apiKeyLabel": "API kulcs", "requestsShort": "{count} igény", @@ -1819,11 +1822,11 @@ "updateStarted": "Frissítés elindult...", "reloadingPageAutomatically": "Oldal automatikus újratöltése...", "providerTopology": "Szolgáltató topológia", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG letöltése (macOS)", "downloadDmgDescription": "Új verzió érhető el az OmniRoute asztali alkalmazásból. Kérjük, töltse le és telepítse a macOS DMG telepítőt a frissítéshez (jelenlegi: v{version}).", "downloadExe": "Letöltés EXE (Windows)", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 4c7c940b4b..2f8ee3c21f 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Pemantau Kesehatan", "reportIssue": "Laporkan masalah", "activeError": "{active} aktif · kesalahan {errors}", + "topologyLegendActive": "Aktif", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Kesalahan", "oauthLabel": "OAuth", "apiKeyLabel": "Kunci API", "requestsShort": "{count} permintaan", @@ -1819,11 +1822,11 @@ "updateStarted": "Pembaruan dimulai...", "reloadingPageAutomatically": "Memuat ulang halaman secara otomatis...", "providerTopology": "Topologi Penyedia", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Unduh DMG (macOS)", "downloadDmgDescription": "Versi baru dari aplikasi desktop OmniRoute tersedia. Silakan unduh dan instal penginstal DMG macOS untuk memperbarui (sekarang: v{version}).", "downloadExe": "Unduh EXE (Windows)", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index c5cc75c684..b25e8169eb 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Monitoraggio della salute", "reportIssue": "Segnala il problema", "activeError": "{active} attivo · {errors} errore", + "topologyLegendActive": "Attivo", + "topologyLegendRecent": "Recente", + "topologyLegendError": "Errore", "oauthLabel": "OAuth", "apiKeyLabel": "Chiave API", "requestsShort": "{count} richieste", @@ -1819,11 +1822,11 @@ "updateStarted": "Aggiornamento avviato...", "reloadingPageAutomatically": "Ricaricamento pagina automaticamente...", "providerTopology": "Topologia del fornitore", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "Richieste recenti", + "recentRequestsEmpty": "Nessuna richiesta per ora.", + "recentRequestsModel": "Modello", + "recentRequestsTokens": "Ingresso / Uscita", + "recentRequestsWhen": "Quando", "downloadDmg": "Scarica DMG (macOS)", "downloadDmgDescription": "È disponibile una nuova versione dell'app desktop OmniRoute. Si prega di scaricare e installare il programma di installazione DMG per macOS per aggiornare (attuale: v{version}).", "downloadExe": "Scarica EXE (Windows)", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 9a15eff936..3f718aaad2 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1807,6 +1807,9 @@ "healthMonitor": "ヘルスモニター", "reportIssue": "問題を報告する", "activeError": "{active} アクティブ · {errors} エラー", + "topologyLegendActive": "アクティブ", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "エラー", "oauthLabel": "OAuth", "apiKeyLabel": "APIキー", "requestsShort": "{count} 件", @@ -1819,11 +1822,11 @@ "updateStarted": "更新を開始しました...", "reloadingPageAutomatically": "ページを自動的に再読み込みしています...", "providerTopology": "プロバイダー トポロジ", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMGをダウンロード (macOS)", "downloadDmgDescription": "OmniRouteデスクトップアプリの新しいバージョンが利用可能です。macOS DMGインストーラーをダウンロードしてインストールし、更新してください(現在のバージョン: v{version})。", "downloadExe": "EXEをダウンロード (Windows)", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index d4d36ccba5..90ee52dfc9 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1807,6 +1807,9 @@ "healthMonitor": "상태 모니터", "reportIssue": "문제 신고", "activeError": "{active} 활성 · {errors} 오류", + "topologyLegendActive": "활성", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "오류", "oauthLabel": "OAuth", "apiKeyLabel": "API 키", "requestsShort": "{count} 요청", @@ -1819,11 +1822,11 @@ "updateStarted": "업데이트 시작됨...", "reloadingPageAutomatically": "페이지를 자동으로 새로고침하는 중...", "providerTopology": "공급자 토폴로지", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG 다운로드 (macOS)", "downloadDmgDescription": "OmniRoute 데스크탑 앱의 새 버전이 출시되었습니다. 업데이트를 위해 macOS DMG 설치 프로그램을 다운로드하고 설치해 주십시오(현재: v{version}).", "downloadExe": "EXE 다운로드 (Windows)", diff --git a/src/i18n/messages/lt.json b/src/i18n/messages/lt.json index 0c6370bc56..b2b8e84166 100644 --- a/src/i18n/messages/lt.json +++ b/src/i18n/messages/lt.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Modelis", "recentRequestsTokens": "Į / Iš", "recentRequestsWhen": "Kada", + "topologyLegendActive": "Aktyvus", + "topologyLegendRecent": "Naujausi", + "topologyLegendError": "Klaida", "downloadDmg": "Atsisiųsti DMG (macOS)", "downloadDmgDescription": "Yra nauja OmniRoute darbalaukio programos versija. Norėdami atnaujinti, atsisiųskite ir įdiekite macOS DMG diegimo failą (esama versija: v{version}).", "downloadExe": "Atsisiųsti EXE (Windows)", diff --git a/src/i18n/messages/lv.json b/src/i18n/messages/lv.json index bacd1f0062..e91c305bb1 100644 --- a/src/i18n/messages/lv.json +++ b/src/i18n/messages/lv.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Modelis", "recentRequestsTokens": "Iekšā / Ārā", "recentRequestsWhen": "Kad", + "topologyLegendActive": "Aktīvs", + "topologyLegendRecent": "Nesenie", + "topologyLegendError": "Kļūda", "downloadDmg": "Lejupielādēt DMG (macOS)", "downloadDmgDescription": "Ir pieejama jauna OmniRoute galddatora lietotnes versija. Lūdzu, lejupielādējiet un instalējiet macOS DMG instalatoru, lai atjauninātu (pašreizējā: v{version}).", "downloadExe": "Lejupielādēt EXE (Windows)", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 26abe41a3f..bacb7c00ba 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "पृष्ठ स्वयंचलितपणे रीलोड करत आहे...", "providerTopology": "प्रदाता टोपोलॉजी", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG डाउनलोड करा (macOS)", "downloadDmgDescription": "OmniRoute डेस्कटॉप अॅपचा एक नवीन आवृत्ती उपलब्ध आहे. कृपया अद्यतन करण्यासाठी macOS DMG इंस्टॉलर डाउनलोड आणि स्थापित करा (सध्याचे: v{version}).", "downloadExe": "EXE डाउनलोड करा (Windows)", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index d09067ec3b..3ff0f00703 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Pemantau Kesihatan", "reportIssue": "Laporkan isu", "activeError": "{active} aktif · {errors} ralat", + "topologyLegendActive": "Aktif", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "ralat", "oauthLabel": "OAuth", "apiKeyLabel": "Kunci API", "requestsShort": "{count} permintaan", @@ -1819,11 +1822,11 @@ "updateStarted": "Kemas kini bermula...", "reloadingPageAutomatically": "Memuat semula halaman secara automatik...", "providerTopology": "Topologi Pembekal", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Muat Turun DMG (macOS)", "downloadDmgDescription": "Versi baru aplikasi desktop OmniRoute tersedia. Sila muat turun dan pasang pemasang DMG macOS untuk mengemas kini (semasa: v{version}).", "downloadExe": "Muat Turun EXE (Windows)", diff --git a/src/i18n/messages/mt.json b/src/i18n/messages/mt.json index 70d5c0f1fd..df7cefd3ea 100644 --- a/src/i18n/messages/mt.json +++ b/src/i18n/messages/mt.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Mudell", "recentRequestsTokens": "Dħul / Ħruġ", "recentRequestsWhen": "Meta", + "topologyLegendActive": "Attiv", + "topologyLegendRecent": "Riċenti", + "topologyLegendError": "Żball", "downloadDmg": "Niżżel id-DMG (macOS)", "downloadDmgDescription": "Verżjoni ġdida tal-app tad-desktop OmniRoute hija disponibbli. Jekk jogħġbok niżżel u installa l-installatur DMG għal macOS biex taġġorna (attwali: v{version}).", "downloadExe": "Niżżel l-EXE (Windows)", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 897eac642b..94c15eae94 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Gezondheidsmonitor", "reportIssue": "Probleem melden", "activeError": "{active} actief · {errors} fout", + "topologyLegendActive": "Actief", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Fout", "oauthLabel": "OAuth", "apiKeyLabel": "API-sleutel", "requestsShort": "{count} vereisten", @@ -1819,11 +1822,11 @@ "updateStarted": "Update gestart...", "reloadingPageAutomatically": "Pagina automatisch herladen...", "providerTopology": "Provider-topologie", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Download DMG (macOS)", "downloadDmgDescription": "Er is een nieuwe versie van de OmniRoute desktopapp beschikbaar. Download en installeer alstublieft de macOS DMG-installatieprogramma om bij te werken (huidig: v{version}).", "downloadExe": "Download EXE (Windows)", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index f6e6519c8a..65f7ff2dd7 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Helsemonitor", "reportIssue": "Rapporter problem", "activeError": "{active} aktiv · {errors} feil", + "topologyLegendActive": "Aktiv", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Feil", "oauthLabel": "OAuth", "apiKeyLabel": "API-nøkkel", "requestsShort": "{count} req", @@ -1819,11 +1822,11 @@ "updateStarted": "Oppdatering startet...", "reloadingPageAutomatically": "Laster siden automatisk på nytt...", "providerTopology": "Leverandørtopologi", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Last ned DMG (macOS)", "downloadDmgDescription": "En ny versjon av OmniRoute skrivebordsappen er tilgjengelig. Vennligst last ned og installer macOS DMG-installasjonsprogrammet for å oppdatere (nåværende: v{version}).", "downloadExe": "Last ned EXE (Windows)", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 3f8f2b49ae..9179c0f80b 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Monitor ng Kalusugan", "reportIssue": "Iulat ang isyu", "activeError": "{active} aktibo · {errors} error", + "topologyLegendActive": "Aktibo", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} mga kahilingan", @@ -1819,11 +1822,11 @@ "updateStarted": "Nagsimula ang pag-update...", "reloadingPageAutomatically": "Awtomatikong nire-reload ang page...", "providerTopology": "Topology ng Provider", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "I-download ang DMG (macOS)", "downloadDmgDescription": "Isang bagong bersyon ng OmniRoute desktop app ang available. Mangyaring i-download at i-install ang macOS DMG installer upang mag-update (kasalukuyan: v{version}).", "downloadExe": "I-download ang EXE (Windows)", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index e986a46958..67d62723fc 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Monitor stanu", "reportIssue": "Zgłoś problem", "activeError": "{active} aktywne · {errors} błąd", + "topologyLegendActive": "Aktywne", + "topologyLegendRecent": "Ostatnie", + "topologyLegendError": "Błąd", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} żądań", @@ -1819,11 +1822,11 @@ "updateStarted": "Rozpoczęto aktualizację...", "reloadingPageAutomatically": "Automatyczne przeładowywanie strony...", "providerTopology": "Topologia Provider", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Pobierz DMG (macOS)", "downloadDmgDescription": "Dostępna jest nowa wersja aplikacji desktopowej OmniRoute. Proszę pobrać i zainstalować instalator DMG dla macOS, aby zaktualizować (aktualna: v{version}).", "downloadExe": "Pobierz EXE (Windows)", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 5156955d9e..1976e9a16d 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -1808,6 +1808,9 @@ "healthMonitor": "Monitor de Saúde", "reportIssue": "Reportar problema", "activeError": "{active} ativo · {errors} erro", + "topologyLegendActive": "Ativo", + "topologyLegendRecent": "Recente", + "topologyLegendError": "Erro", "oauthLabel": "OAuth", "apiKeyLabel": "Chave de API", "requestsShort": "{count} reqs", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index ded9223ecd..0aa621b2a1 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Monitor de Saúde", "reportIssue": "Informar problema", "activeError": "{active} ativo · Erro {errors}", + "topologyLegendActive": "Ativo", + "topologyLegendRecent": "Recente", + "topologyLegendError": "Erro", "oauthLabel": "OAuth", "apiKeyLabel": "Chave de API", "requestsShort": "{count} requisitos", @@ -1819,11 +1822,11 @@ "updateStarted": "Atualização iniciada...", "reloadingPageAutomatically": "Recarregando a página automaticamente...", "providerTopology": "Topologia do provedor", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "Pedidos recentes", + "recentRequestsEmpty": "Ainda não há pedidos.", + "recentRequestsModel": "Modelo", + "recentRequestsTokens": "Entrada / Saída", + "recentRequestsWhen": "Quando", "downloadDmg": "Transferir DMG (macOS)", "downloadDmgDescription": "Uma nova versão da aplicação de desktop OmniRoute está disponível. Por favor, faça o download e instale o instalador DMG para macOS para atualizar (atual: v{version}).", "downloadExe": "Descarregar EXE (Windows)", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index aefd886507..a55d9eee8b 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Monitor de sănătate", "reportIssue": "Raportați problema", "activeError": "{active} activ · {errors} eroare", + "topologyLegendActive": "Activ", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Eroare", "oauthLabel": "OAuth", "apiKeyLabel": "Cheia API", "requestsShort": "{count} solicită", @@ -1819,11 +1822,11 @@ "updateStarted": "Actualizarea a început...", "reloadingPageAutomatically": "Se reîncarcă pagina automat...", "providerTopology": "Topologia furnizorului", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Descarcă DMG (macOS)", "downloadDmgDescription": "O nouă versiune a aplicației desktop OmniRoute este disponibilă. Vă rugăm să descărcați și să instalați programul de instalare DMG pentru macOS pentru a actualiza (curent: v{version}).", "downloadExe": "Descarcă EXE (Windows)", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 39593d9562..b4637e534a 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Монитор здоровья", "reportIssue": "Сообщить о проблеме", "activeError": "{active} активен · {errors} ошибка", + "topologyLegendActive": "Активный", + "topologyLegendRecent": "Недавнее", + "topologyLegendError": "Ошибка", "oauthLabel": "OAuth", "apiKeyLabel": "API-ключ", "requestsShort": "{count} требуется", @@ -1819,11 +1822,11 @@ "updateStarted": "Обновление начато...", "reloadingPageAutomatically": "Автоматическая перезагрузка страницы...", "providerTopology": "Топология провайдера", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Скачать DMG (macOS)", "downloadDmgDescription": "Доступна новая версия настольного приложения OmniRoute. Пожалуйста, загрузите и установите установщик DMG для macOS, чтобы обновить (текущая: v{version}).", "downloadExe": "Скачать EXE (Windows)", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index cf207dca50..501d36a395 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Nahlásiť problém", "activeError": "{active} aktívny · {errors} chyba", + "topologyLegendActive": "Aktívne", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Chyba", "oauthLabel": "OAuth", "apiKeyLabel": "API kľúč", "requestsShort": "{count} req", @@ -1819,11 +1822,11 @@ "updateStarted": "Aktualizácia spustená...", "reloadingPageAutomatically": "Automaticky sa znova načítava stránka...", "providerTopology": "Topológia poskytovateľa", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Stiahnuť DMG (macOS)", "downloadDmgDescription": "Nová verzia desktopovej aplikácie OmniRoute je k dispozícii. Prosím, stiahnite a nainštalujte inštalátor DMG pre macOS na aktualizáciu (aktuálna: v{version}).", "downloadExe": "Stiahnuť EXE (Windows)", diff --git a/src/i18n/messages/sl.json b/src/i18n/messages/sl.json index e8f192e07c..6266e4d2df 100644 --- a/src/i18n/messages/sl.json +++ b/src/i18n/messages/sl.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Model", "recentRequestsTokens": "Vhod / izhod", "recentRequestsWhen": "Čas", + "topologyLegendActive": "Aktivno", + "topologyLegendRecent": "Nedavno", + "topologyLegendError": "Napaka", "downloadDmg": "Prenesi DMG (macOS)", "downloadDmgDescription": "Na voljo je nova različica namizne aplikacije OmniRoute. Za posodobitev prenesite in namestite namestitveni program DMG za macOS (trenutno: v{version}).", "downloadExe": "Prenesi EXE (Windows)", diff --git a/src/i18n/messages/sr.json b/src/i18n/messages/sr.json index 21dd809eca..29049d46cc 100644 --- a/src/i18n/messages/sr.json +++ b/src/i18n/messages/sr.json @@ -1824,6 +1824,9 @@ "recentRequestsModel": "Модел", "recentRequestsTokens": "Улаз / Излаз", "recentRequestsWhen": "Када", + "topologyLegendActive": "Активно", + "topologyLegendRecent": "Недавно", + "topologyLegendError": "Грешка", "downloadDmg": "Преузми DMG (macOS)", "downloadDmgDescription": "Доступна је нова верзија OmniRoute десктоп апликације. Молимо преузмите и инсталирајте macOS DMG инсталер да бисте ажурирали (тренутно: v{version}).", "downloadExe": "Преузми EXE (Windows)", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index b15de5fa3e..d251e49453 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Hälsoövervakare", "reportIssue": "Rapportera problem", "activeError": "{active} aktiv · {errors} fel", + "topologyLegendActive": "Aktiv", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Fel", "oauthLabel": "OAuth", "apiKeyLabel": "API-nyckel", "requestsShort": "{count} krav", @@ -1819,11 +1822,11 @@ "updateStarted": "Uppdatering startade...", "reloadingPageAutomatically": "Laddar om sidan automatiskt...", "providerTopology": "Leverantörstopologi", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Ladda ner DMG (macOS)", "downloadDmgDescription": "En ny version av OmniRoute-skrivbordsappen är tillgänglig. Vänligen ladda ner och installera macOS DMG-installationsprogrammet för att uppdatera (nuvarande: v{version}).", "downloadExe": "Ladda ner EXE (Windows)", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 1c81e25a00..de2e0bd5a2 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "Inapakia upya ukurasa kiotomatiki...", "providerTopology": "Topolojia ya mtoaji", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Pakua DMG (macOS)", "downloadDmgDescription": "Toleo jipya la programu ya desktop ya OmniRoute linapatikana. Tafadhali pakua na sakinisha msanidi wa DMG wa macOS ili kusasisha (sasa: v{version}).", "downloadExe": "Pakua EXE (Windows)", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index 7313728f83..bf97cd4ede 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "தானாக பக்கத்தை மீண்டும் ஏற்றுகிறது...", "providerTopology": "வழங்குநர் இடவியல்", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG ஐ பதிவிறக்கம் செய்யவும் (macOS)", "downloadDmgDescription": "OmniRoute டெஸ்க்டாப் செயலியின் புதிய பதிப்பு கிடைக்கிறது. தயவுசெய்து புதுப்பிக்க macOS DMG நிறுவுநரை பதிவிறக்கம் செய்து நிறுவவும் (தற்போதைய: v{version}).", "downloadExe": "EXE பதிவிறக்கம் (Windows)", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 9c8fefb5ac..abbfbe3fc2 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "పేజీని స్వయంచాలకంగా రీలోడ్ చేస్తోంది...", "providerTopology": "ప్రొవైడర్ టోపాలజీ", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG డౌన్‌లోడ్ చేయండి (macOS)", "downloadDmgDescription": "ఒక కొత్త సంచిక OmniRoute డెస్క్‌టాప్ యాప్ అందుబాటులో ఉంది. దయచేసి నవీకరించడానికి macOS DMG ఇన్‌స్టాలర్‌ను డౌన్‌లోడ్ చేసి ఇన్‌స్టాల్ చేయండి (ప్రస్తుత: v{version}).", "downloadExe": "EXE డౌన్‌లోడ్ చేయండి (విండోస్)", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 67cb1ca41c..d7a972ec27 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -1807,6 +1807,9 @@ "healthMonitor": "การตรวจสุขภาพ", "reportIssue": "รายงานปัญหา", "activeError": "{active} ใช้งานอยู่ · ข้อผิดพลาด {errors}", + "topologyLegendActive": "ใช้งานอยู่", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "เกิดข้อผิดพลาด", "oauthLabel": "OAuth", "apiKeyLabel": "คีย์ API", "requestsShort": "{count} ความต้องการ", @@ -1819,11 +1822,11 @@ "updateStarted": "เริ่มการอัพเดต...", "reloadingPageAutomatically": "กำลังโหลดหน้าซ้ำโดยอัตโนมัติ...", "providerTopology": "โทโพโลยีของผู้ให้บริการ", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "ดาวน์โหลด DMG (macOS)", "downloadDmgDescription": "มีเวอร์ชันใหม่ของแอปเดสก์ท็อป OmniRoute พร้อมใช้งาน กรุณาดาวน์โหลดและติดตั้งตัวติดตั้ง macOS DMG เพื่อทำการอัปเดต (ปัจจุบัน: v{version}).", "downloadExe": "ดาวน์โหลด EXE (Windows)", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index b034c56c9f..7892a3f00b 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Sağlık Monitörü", "reportIssue": "Sorunu bildir", "activeError": "{active} aktif · {errors} hata", + "topologyLegendActive": "Aktif", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Hata", "oauthLabel": "OAuth", "apiKeyLabel": "API Anahtarı", "requestsShort": "{count} istek", @@ -1819,11 +1822,11 @@ "updateStarted": "Güncelleme başladı...", "reloadingPageAutomatically": "Sayfa otomatik olarak yeniden yükleniyor...", "providerTopology": "Sağlayıcı Topolojisi", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG İndir (macOS)", "downloadDmgDescription": "OmniRoute masaüstü uygulamasının yeni bir sürümü mevcut. Lütfen güncellemek için macOS DMG yükleyicisini indirin ve kurun (mevcut: v{version}).", "downloadExe": "EXE İndir (Windows)", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index f006bcf52b..e86f5fd0a8 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Монітор здоров'я", "reportIssue": "Повідомити про проблему", "activeError": "{active} активний · {errors} помилка", + "topologyLegendActive": "Активний", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "Помилка", "oauthLabel": "OAuth", "apiKeyLabel": "Ключ API", "requestsShort": "{count} вимагається", @@ -1819,11 +1822,11 @@ "updateStarted": "Оновлення розпочато...", "reloadingPageAutomatically": "Автоматичне перезавантаження сторінки...", "providerTopology": "Топологія провайдера", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "Завантажити DMG (macOS)", "downloadDmgDescription": "Доступна нова версія настільного додатку OmniRoute. Будь ласка, завантажте та встановіть установник DMG для macOS, щоб оновити (поточна: v{version}).", "downloadExe": "Завантажити EXE (Windows)", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 3ac0fe80c9..9f240aa287 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -1807,6 +1807,9 @@ "healthMonitor": "Health Monitor", "reportIssue": "Report issue", "activeError": "{active} active · {errors} error", + "topologyLegendActive": "__MISSING__:Active", + "topologyLegendRecent": "__MISSING__:Recent", + "topologyLegendError": "__MISSING__:Error", "oauthLabel": "OAuth", "apiKeyLabel": "API Key", "requestsShort": "{count} reqs", @@ -1819,11 +1822,11 @@ "updateStarted": "Update started...", "reloadingPageAutomatically": "صفحہ خودکار طور پر دوبارہ لوڈ ہو رہا ہے...", "providerTopology": "فراہم کنندہ ٹوپولوجی", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "DMG ڈاؤن لوڈ کریں (macOS)", "downloadDmgDescription": "OmniRoute ڈیسک ٹاپ ایپ کا نیا ورژن دستیاب ہے۔ براہ کرم اپ ڈیٹ کرنے کے لیے macOS DMG انسٹالر ڈاؤن لوڈ اور انسٹال کریں (موجودہ: v{version})۔", "downloadExe": "EXE ڈاؤن لوڈ کریں (ونڈوز)", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index b6d7e2f640..155d41ce84 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -1808,6 +1808,9 @@ "healthMonitor": "Trình theo dõi tình trạng", "reportIssue": "Báo cáo sự cố", "activeError": "{active} đang hoạt động · {errors} lỗi", + "topologyLegendActive": "Đang hoạt động", + "topologyLegendRecent": "Gần đây", + "topologyLegendError": "Lỗi", "oauthLabel": "OAuth", "apiKeyLabel": "Khóa API", "requestsShort": "{count} yêu cầu", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 6454daa09b..346831b001 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1807,6 +1807,9 @@ "healthMonitor": "健康监测", "reportIssue": "报告问题", "activeError": "{active} 有效 · {errors} 错误", + "topologyLegendActive": "启用中", + "topologyLegendRecent": "最近", + "topologyLegendError": "错误", "oauthLabel": "OAuth", "apiKeyLabel": "API密钥", "requestsShort": "{count} 次请求", @@ -1819,11 +1822,11 @@ "updateStarted": "更新已开始...", "reloadingPageAutomatically": "自动重新加载页面...", "providerTopology": "提供者拓扑", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "下载 DMG (macOS)", "downloadDmgDescription": "OmniRoute 桌面应用程序的新版本可用。请下载并安装 macOS DMG 安装程序以进行更新(当前版本:v{version})。", "downloadExe": "下载 EXE(Windows)", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 7c335e49bf..63f8e394bc 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1807,6 +1807,9 @@ "healthMonitor": "健康監測", "reportIssue": "報告問題", "activeError": "{active} 有效 · {errors} 錯誤", + "topologyLegendActive": "啟用中", + "topologyLegendRecent": "最近", + "topologyLegendError": "錯誤", "oauthLabel": "OAuth", "apiKeyLabel": "API金鑰", "requestsShort": "{count} 次請求", @@ -1819,11 +1822,11 @@ "updateStarted": "更新已開始...", "reloadingPageAutomatically": "自動重新載入頁面...", "providerTopology": "提供者拓撲", - "recentRequests": "Recent Requests", - "recentRequestsEmpty": "No requests yet.", - "recentRequestsModel": "Model", - "recentRequestsTokens": "In / Out", - "recentRequestsWhen": "When", + "recentRequests": "__MISSING__:Recent Requests", + "recentRequestsEmpty": "__MISSING__:No requests yet.", + "recentRequestsModel": "__MISSING__:Model", + "recentRequestsTokens": "__MISSING__:In / Out", + "recentRequestsWhen": "__MISSING__:When", "downloadDmg": "下載 DMG (macOS)", "downloadDmgDescription": "OmniRoute 桌面應用程式的新版本已經可用。請下載並安裝 macOS DMG 安裝程式以進行更新(目前版本:v{version})。", "downloadExe": "下載 EXE (Windows)", diff --git a/tests/unit/i18n-home-recent-requests-topology-legend.test.ts b/tests/unit/i18n-home-recent-requests-topology-legend.test.ts new file mode 100644 index 0000000000..f9a9fdbd73 --- /dev/null +++ b/tests/unit/i18n-home-recent-requests-topology-legend.test.ts @@ -0,0 +1,134 @@ +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { test } from "node:test"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const MESSAGES_DIR = path.join(repoRoot, "src", "i18n", "messages"); +const PLACEHOLDER_PREFIX = "__MISSING__:"; + +function readMessages(locale: string): Record { + return JSON.parse(readFileSync(path.join(MESSAGES_DIR, `${locale}.json`), "utf8")) as Record< + string, + unknown + >; +} + +function getMessage(messages: Record, dottedKey: string): unknown { + return dottedKey.split(".").reduce((value, segment) => { + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; + return (value as Record)[segment]; + }, messages); +} + +const allLocales = readdirSync(MESSAGES_DIR) + .filter((file) => file.endsWith(".json")) + .map((file) => file.slice(0, -".json".length)); + +// The home "Recent Requests" panel (#10900) shipped its five catalog keys as verbatim English +// copies in 39 of 41 non-English locales, so the widget rendered in English on every +// translated dashboard (title, "Model", "In / Out", "When", empty state). The topology legend +// borrowed `settings.recent` (the memory-retrieval window label, also an English copy) and +// `analytics.modelStatusError`, which mixed languages and casing ("Activo · Recent · error"). +const RECENT_REQUESTS_KEYS = [ + "home.recentRequests", + "home.recentRequestsEmpty", + "home.recentRequestsModel", + "home.recentRequestsTokens", + "home.recentRequestsWhen", +]; +const TOPOLOGY_LEGEND_KEYS = [ + "home.topologyLegendActive", + "home.topologyLegendRecent", + "home.topologyLegendError", +]; +const HOME_WIDGET_KEYS = [...RECENT_REQUESTS_KEYS, ...TOPOLOGY_LEGEND_KEYS]; + +// Locales that must carry a real translation, never an English copy nor a placeholder. +const TRANSLATED_LOCALES = ["es", "pt", "pt-BR", "fr", "de", "it", "vi"]; +// Genuine cognates: the correct translation happens to spell exactly like the English value. +const COGNATES = new Set([ + "es.home.topologyLegendError", + // "Model" is the correct Croatian and Slovenian word; there is nothing to translate. + "hr.home.recentRequestsModel", + "sl.home.recentRequestsModel", +]); + +test("home widget keys exist as non-empty strings in every locale catalog", () => { + assert.ok(allLocales.length >= 42, `expected the 42 locale catalogs, found ${allLocales.length}`); + for (const locale of allLocales) { + const messages = readMessages(locale); + for (const key of HOME_WIDGET_KEYS) { + const value = getMessage(messages, key); + assert.equal(typeof value, "string", `${locale}.${key} must exist`); + assert.notEqual((value as string).trim(), "", `${locale}.${key} must not be empty`); + } + } +}); + +test("home widget keys are translated (not English copies) in the maintained locales", () => { + const en = readMessages("en"); + for (const locale of TRANSLATED_LOCALES) { + const messages = readMessages(locale); + for (const key of HOME_WIDGET_KEYS) { + const value = getMessage(messages, key) as string; + const english = getMessage(en, key) as string; + assert.ok( + !value.startsWith(PLACEHOLDER_PREFIX), + `${locale}.${key} must not be a ${PLACEHOLDER_PREFIX} placeholder` + ); + if (COGNATES.has(`${locale}.${key}`)) continue; + assert.notEqual(value, english, `${locale}.${key} must not be the verbatim English value`); + } + } +}); + +test("no locale keeps a silent English copy of the Recent Requests keys", () => { + // A verbatim copy of the English value is invisible to every i18n gate (it counts as + // "covered"); either translate it or mark it __MISSING__ so the pipeline can see it. + const en = readMessages("en"); + for (const locale of allLocales) { + if (locale === "en") continue; + const messages = readMessages(locale); + for (const key of RECENT_REQUESTS_KEYS) { + const value = getMessage(messages, key) as string; + const english = getMessage(en, key) as string; + assert.ok( + value !== english || + value.startsWith(PLACEHOLDER_PREFIX) || + COGNATES.has(`${locale}.${key}`), + `${locale}.${key} is a verbatim English copy ("${english}")` + ); + } + } +}); + +test("topology legend reads its labels from the home namespace, not memory settings", () => { + const source = readFileSync( + path.join(repoRoot, "src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx"), + "utf8" + ); + assert.doesNotMatch(source, /tSettings\("recent"\)/, "legend must not borrow settings.recent"); + assert.doesNotMatch( + source, + /tAnalytics\("modelStatusError"\)/, + "legend must not borrow analytics.modelStatusError" + ); + for (const key of ["topologyLegendActive", "topologyLegendRecent", "topologyLegendError"]) { + assert.match(source, new RegExp(`t\\("${key}"\\)`), `legend must use home.${key}`); + } +}); + +test("topology legend casing matches across languages in the maintained locales", () => { + // The legend is a row of three labels; they must share capitalisation within a locale. + for (const locale of ["en", ...TRANSLATED_LOCALES]) { + const messages = readMessages(locale); + const labels = TOPOLOGY_LEGEND_KEYS.map((key) => getMessage(messages, key) as string); + const upperInitial = labels.map((label) => /^\p{Lu}/u.test(label)); + assert.ok( + upperInitial.every((flag) => flag === upperInitial[0]), + `${locale} legend mixes capitalisation: ${JSON.stringify(labels)}` + ); + } +}); From ba1ee6617478f6d0bea819e8574c96f85ad0621e Mon Sep 17 00:00:00 2001 From: Paco Cartones <253313177+pacocartones@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:13:18 +0200 Subject: [PATCH 20/20] fix(i18n): quote raw in the auto-sync profiles description (#12549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged for the half that is still live. The 42 locale catalogs were fixed upstream while this sat open — all 51 now carry `''` — but the TypeScript default at `featureFlagDefinitions.ts:625` still had the raw tag, so the Feature Flags card kept failing to compile wherever the default is the source. Against the current tip this PR lands exactly three files: that one-character fix, the changelog fragment, and your 135-line regression test, with zero locale files touched. The analysis is what makes it worth keeping. `next-intl` parsing `` as a rich-text tag, `FeatureFlagsGrid.tsx` rendering the description through plain `t()` with no tag element, and the result being `INVALID_MESSAGE: UNCLOSED_TAG` — the card showing the raw key instead of the description, in every language — is a failure mode that is easy to misread as a missing translation. Verifying it against `use-intl`'s `development` build, the one Turbopack dev mode actually loads, is the detail that makes the reproduction trustworthy. Wrapping in ICU single quotes matches what #12369 did for `ccOnboardingKeyPlaceholder`. 5/5 on the regression suite against the tip. --- ⚠️ base-red inherited: #12732. Thanks @pacocartones — 18 more of yours merged today. --- .../12549-i18n-escape-raw-name-tag-12505.md | 1 + .../constants/featureFlagDefinitions.ts | 2 +- ...-flag-auto-sync-profiles-tag-12505.test.ts | 135 ++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/12549-i18n-escape-raw-name-tag-12505.md create mode 100644 tests/unit/i18n-feature-flag-auto-sync-profiles-tag-12505.test.ts diff --git a/changelog.d/fixes/12549-i18n-escape-raw-name-tag-12505.md b/changelog.d/fixes/12549-i18n-escape-raw-name-tag-12505.md new file mode 100644 index 0000000000..6de3771893 --- /dev/null +++ b/changelog.d/fixes/12549-i18n-escape-raw-name-tag-12505.md @@ -0,0 +1 @@ +- **fix(i18n):** Wrap the `~/.claude/profiles//settings.json` placeholder in ICU single quotes in the `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` feature-flag description across all 42 locales and the TypeScript default, so next-intl no longer fails with `INVALID_MESSAGE: UNCLOSED_TAG` and the Feature Flags card shows the description instead of the raw key (#12549 — thanks @pacocartones) diff --git a/src/shared/constants/featureFlagDefinitions.ts b/src/shared/constants/featureFlagDefinitions.ts index 88ace23020..8271d21c5b 100644 --- a/src/shared/constants/featureFlagDefinitions.ts +++ b/src/shared/constants/featureFlagDefinitions.ts @@ -622,7 +622,7 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [ key: "OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES", label: "Auto-Sync Claude Code Profiles", description: - "After a provider model sync, automatically (re)write ~/.claude/profiles//settings.json Claude Code profiles from the live catalog. Never changes the active/default Claude config. Off by default.", + "After a provider model sync, automatically (re)write ~/.claude/profiles/''/settings.json Claude Code profiles from the live catalog. Never changes the active/default Claude config. Off by default.", descriptionI18nKey: "featureFlagOmnirouteAutoSyncClaudeProfilesDescription", category: "cli", defaultValue: "false", diff --git a/tests/unit/i18n-feature-flag-auto-sync-profiles-tag-12505.test.ts b/tests/unit/i18n-feature-flag-auto-sync-profiles-tag-12505.test.ts new file mode 100644 index 0000000000..12beeb7119 --- /dev/null +++ b/tests/unit/i18n-feature-flag-auto-sync-profiles-tag-12505.test.ts @@ -0,0 +1,135 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { parse } from "@formatjs/icu-messageformat-parser"; +import { createTranslator } from "next-intl"; +import i18nConfig from "../../config/i18n.json" with { type: "json" }; + +const { FEATURE_FLAG_DEFINITIONS } = + await import("../../src/shared/constants/featureFlagDefinitions.ts"); + +const MESSAGES_DIR = path.resolve("src/i18n/messages"); +const FLAG_KEY = "OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES"; +const MESSAGE_KEY = `definitions.${FLAG_KEY}.description`; +const RAW_PATH = "profiles//"; +const QUOTED_PATH = "profiles/''/"; +const ENTITY_PATH = "profiles/<name>/"; +const RENDERED_PATH = "~/.claude/profiles//settings.json"; + +/** + * Regression guard for #12505 (INVALID_MESSAGE: UNCLOSED_TAG on the Feature + * Flags page). The `featureFlags.definitions.OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES.description` + * message carried a literal `~/.claude/profiles//settings.json` path. + * next-intl parses `` as a rich-text tag, no tag element is ever passed + * by `FeatureFlagsGrid.tsx` (plain `t()`), so the message failed to compile and + * the card fell back to the raw key in every locale. + * + * Fix: the placeholder is wrapped in ICU single quotes (`''`) so the + * angle brackets render literally. HTML entities are not an option here: the + * value is a real file path shown to the user, and `t()` returns entities + * verbatim (`<name>` would be displayed as-is). + */ + +function flatten(obj: Record, prefix = ""): Record { + const out: Record = {}; + for (const k of Object.keys(obj)) { + const key = prefix ? `${prefix}.${k}` : k; + const v = obj[k]; + if (v && typeof v === "object" && !Array.isArray(v)) { + Object.assign(out, flatten(v as Record, key)); + } else { + out[key] = v; + } + } + return out; +} + +describe(`i18n — ${FLAG_KEY} description UNCLOSED_TAG regression (#12505)`, () => { + const localeFiles = fs + .readdirSync(MESSAGES_DIR) + .filter((f) => f.endsWith(".json")) + .sort(); + const expectedCount = i18nConfig.locales.length; + + function readDescription(file: string): string { + const raw = fs.readFileSync(path.join(MESSAGES_DIR, file), "utf8"); + assert.notEqual(raw.charCodeAt(0), 0xfeff, `${file}: starts with BOM (U+FEFF)`); + const flat = flatten(JSON.parse(raw) as Record); + const value = flat[`featureFlags.${MESSAGE_KEY}`]; + assert.equal(typeof value, "string", `${file}: featureFlags.${MESSAGE_KEY} must be a string`); + return value as string; + } + + it(`the description exists in all ${expectedCount} locales`, () => { + assert.equal(localeFiles.length, expectedCount); + for (const file of localeFiles) { + readDescription(file); + } + }); + + it("every locale value parses as an ICU message (no unclosed tag)", () => { + const failures: string[] = []; + for (const file of localeFiles) { + try { + parse(readDescription(file), { captureLocation: false, shouldParseSkeletons: true }); + } catch (error) { + failures.push(`${file}: ${error instanceof Error ? error.message : String(error)}`); + } + } + assert.deepEqual(failures, [], `ICU parse failures: ${failures.slice(0, 5).join("; ")}`); + }); + + it("every locale wraps the profile path placeholder in ICU single quotes", () => { + const offenders: string[] = []; + for (const file of localeFiles) { + const value = readDescription(file); + if (value.includes(RAW_PATH)) offenders.push(`${file}: raw ${RAW_PATH}`); + if (value.includes(ENTITY_PATH)) offenders.push(`${file}: entity ${ENTITY_PATH}`); + if (!value.includes(QUOTED_PATH)) offenders.push(`${file}: missing ${QUOTED_PATH}`); + } + assert.deepEqual(offenders, [], offenders.slice(0, 10).join(", ")); + }); + + it("createTranslator renders the literal path in every locale without INVALID_MESSAGE", () => { + const errors: string[] = []; + const wrong: string[] = []; + for (const file of localeFiles) { + const locale = file.replace(/\.json$/, ""); + const messages = JSON.parse(fs.readFileSync(path.join(MESSAGES_DIR, file), "utf8")); + const t = createTranslator({ + locale, + messages, + namespace: "featureFlags", + onError: (err: { code?: string; originalMessage?: string; message?: string }) => { + errors.push(`${locale}: ${err.code}: ${err.originalMessage ?? err.message}`); + }, + }); + assert.ok(t.has(MESSAGE_KEY), `${locale}: t.has(${MESSAGE_KEY}) must be true`); + const rendered = t(MESSAGE_KEY); + if (!rendered.includes(RENDERED_PATH)) { + wrong.push(`${locale}: ${rendered.slice(0, 80)}`); + } + } + assert.deepEqual(errors, [], `next-intl errors: ${errors.slice(0, 5).join("; ")}`); + assert.deepEqual( + wrong, + [], + `rendered text lost the literal path: ${wrong.slice(0, 5).join("; ")}` + ); + }); + + it("the TypeScript default description parses and uses the same quoting", () => { + const flag = FEATURE_FLAG_DEFINITIONS.find((f) => f.key === FLAG_KEY); + assert.ok(flag, `${FLAG_KEY} must be defined`); + assert.doesNotThrow(() => + parse(flag.description, { captureLocation: false, shouldParseSkeletons: true }) + ); + assert.ok(flag.description.includes(QUOTED_PATH), `default must contain ${QUOTED_PATH}`); + assert.equal( + flag.description.includes(RAW_PATH), + false, + `default must not contain ${RAW_PATH}` + ); + }); +});