diff --git a/AGENTS.md b/AGENTS.md index 1c0a979789b..f98f367e52e 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 (171 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (172 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 8753569c474..3c8bc268975 100644 --- a/README.md +++ b/README.md @@ -1244,7 +1244,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, 171 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 172 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/features/12786-manifest-free-tier-capability.md b/changelog.d/features/12786-manifest-free-tier-capability.md new file mode 100644 index 00000000000..c075841a94d --- /dev/null +++ b/changelog.d/features/12786-manifest-free-tier-capability.md @@ -0,0 +1 @@ +- **feat(providers):** advertise a `free-tier` capability in the provider plugin manifest for every provider with documented free models, so sidecars and dashboards can filter free-capable providers without reading the quota catalog ([#12786](https://github.com/diegosouzapw/OmniRoute/pull/12786)) — thanks @maxmad64bis diff --git a/changelog.d/fixes/12731-scoring-packs-quality-reliability.md b/changelog.d/fixes/12731-scoring-packs-quality-reliability.md new file mode 100644 index 00000000000..32033d44561 --- /dev/null +++ b/changelog.d/fixes/12731-scoring-packs-quality-reliability.md @@ -0,0 +1 @@ +- **fix(auto-combo):** every mode pack now carries `quality` and `reliability` weights (`quality` 0.02, 0.03 in `quality-first`; `reliability` 0.03, 0.04 in `reliability-first`), so selecting a pack no longer silences either signal; `DEFAULT_WEIGHTS` is unchanged ([#12731](https://github.com/diegosouzapw/OmniRoute/pull/12731)) — thanks @maxmad64bis diff --git a/changelog.d/fixes/12744-free-predicate-guard.md b/changelog.d/fixes/12744-free-predicate-guard.md new file mode 100644 index 00000000000..a0f979a149e --- /dev/null +++ b/changelog.d/fixes/12744-free-predicate-guard.md @@ -0,0 +1 @@ +- **fix(models):** stop treating provider-supplied `isFree:true`/`:free`/`0/0` flags as trusted unless the provider has a documented free tier — only the shipped free-tier catalog decides otherwise; fetched `isFree:true` trusted only for a free-tier provider, custom `isFree:true` via a trusted path ([#12744](https://github.com/diegosouzapw/OmniRoute/pull/12744)) — thanks @maxmad64bis diff --git a/changelog.d/fixes/12790-provider-order-single-source.md b/changelog.d/fixes/12790-provider-order-single-source.md new file mode 100644 index 00000000000..6a2c5873753 --- /dev/null +++ b/changelog.d/fixes/12790-provider-order-single-source.md @@ -0,0 +1 @@ +- **fix(providers):** single source for provider order with `xao` ranking alongside `xai-oauth` ([#12790](https://github.com/diegosouzapw/OmniRoute/pull/12790)) — thanks @maxmad64bis diff --git a/changelog.d/fixes/12792-pricing-valuation.md b/changelog.d/fixes/12792-pricing-valuation.md new file mode 100644 index 00000000000..7eea0707262 --- /dev/null +++ b/changelog.d/fixes/12792-pricing-valuation.md @@ -0,0 +1 @@ +- **fix(routing):** off-table models check the free-model catalog before inheriting premium prices, latency bootstraps from the observed pool median, async tiers read live database pricing with a 90-day freshness gate, and the tier cache invalidates on every pricing write ([#12792](https://github.com/diegosouzapw/OmniRoute/pull/12792)) — thanks @maxmad64bis diff --git a/changelog.d/fixes/12794-snapshot-scoring.md b/changelog.d/fixes/12794-snapshot-scoring.md new file mode 100644 index 00000000000..18e0b55f228 --- /dev/null +++ b/changelog.d/fixes/12794-snapshot-scoring.md @@ -0,0 +1 @@ +- **fix(auto-combo):** snapshot scoring uses the observed per-connection account tier and per-model quality instead of neutral constants, and circuit-open providers rank lower at build time ([#12794](https://github.com/diegosouzapw/OmniRoute/pull/12794)) — thanks @maxmad64bis diff --git a/changelog.d/maintenance/doc-counts-172-migrations.md b/changelog.d/maintenance/doc-counts-172-migrations.md new file mode 100644 index 00000000000..4d6d36e6cc9 --- /dev/null +++ b/changelog.d/maintenance/doc-counts-172-migrations.md @@ -0,0 +1 @@ +- Update the documented migration count to 172 after the call-logs provider-stats indexes landed. diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 20740400106..849047598f2 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -411,11 +411,6 @@ "count": 1 } }, - "open-sse/services/autoCombo/__tests__/autoCombo.test.ts": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, "open-sse/services/autoCombo/chaosEngine.ts": { "@typescript-eslint/no-unused-vars": { "count": 2 diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 49c5f747242..41d80fbff62 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -467,7 +467,8 @@ "src/sse/handlers/chat.ts": 2458, "src/sse/services/auth.ts": 3450, "tests/unit/account-fallback-service.test.ts": 2453, - "tests/unit/provider-validation-specialty.test.ts": 4656 + "tests/unit/provider-validation-specialty.test.ts": 4656, + "open-sse/services/autoCombo/virtualFactory.ts": 1207 }, "_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.", "_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).", @@ -652,5 +653,6 @@ "_rebaseline_2026_09_03_houminxi_combo_stacked": "Leva HouMinXi (#12624 #12626 #12632 #12637): open-sse/services/combo.ts 4075->4080 (+5), medido no tip com os quatro mergeados. Cada PR registrou o proprio crescimento contra o tip de onde forkou (o #12637 ja subira o cap para 4075); as 5 linhas restantes so aparecem quando eles empilham, porque mais de um toca o mesmo chokepoint de scoring reset-aware em combo.ts. Fiacao em ponto existente, sem extracao possivel sem partir a funcao de selecao de alvos. Coberto por combo-strategies e reset-aware-request-scope-12600 (119/119 focados na leva).", "_rebaseline_2026_09_04_12641_continuation_effective_input": "PR #12641 crescimento proprio: src/sse/handlers/chat.ts 2450->2454 (+4). A continuacao por previous_response_id encadeava a partir de clientRawRequest.body.input, que e capturado ANTES da reconstrucao do proprio chat.ts; quando o turno anterior ja era uma continuacao, esse campo guarda so o delta do cliente, e o erro se acumulava a cada salto ate a reconstrucao virar itens de tool sem prefixo. Persistir o input EFETIVO exige as linhas no ponto onde a reconstrucao termina, dentro do fluxo de despacho. Coberto por tests/unit/responses-continuation-store.test.ts (22/22 focados na leva).", "_rebaseline_2026_09_05_12671_combos_usage_guide_external_store": "combos/page.tsx 5018 -> 5066: #12671 replaces the effect-based localStorage read with useSyncExternalStore; the +48 lines are the store helpers (subscribe/getSnapshot/getServerSnapshot/emit) hoisted to module scope, which is the sanctioned shape and what let the react-hooks/set-state-in-effect suppression be dropped.", - "_rebaseline_2026_09_07_chatcore_nonstreaming_regression_fixes": "Own growth: open-sse/handlers/chatCore.ts 5984->6021 (+37). Two of my own PRs on top of #12867: #12963 pins the ok variant of the non-streaming leg result in its own binding (the discriminated-union narrowing was lost across the tool-loop reassignment, 13 TS2339 under tsconfig.typecheck-api.json), and #12990 restores four behaviours the same refactor dropped — abort classification through isLocalStreamLifecycleError, the omitted synthetic clientResponse, the claudePromptCacheLogMeta rebuild on the leg path, and the lazy fail-closed fence identity. Irreducible at the existing chokepoints: each edit sits where chatCore already owns the decision, and the helpers themselves (nonStreamingProviderLeg.ts, serverOwnedToolLoopWire.ts) are under cap. Covered by tests/unit/chatcore-translation-paths.test.ts (72/74; the 2 open are issue #13043)." + "_rebaseline_2026_09_07_chatcore_nonstreaming_regression_fixes": "Own growth: open-sse/handlers/chatCore.ts 5984->6021 (+37). Two of my own PRs on top of #12867: #12963 pins the ok variant of the non-streaming leg result in its own binding (the discriminated-union narrowing was lost across the tool-loop reassignment, 13 TS2339 under tsconfig.typecheck-api.json), and #12990 restores four behaviours the same refactor dropped — abort classification through isLocalStreamLifecycleError, the omitted synthetic clientResponse, the claudePromptCacheLogMeta rebuild on the leg path, and the lazy fail-closed fence identity. Irreducible at the existing chokepoints: each edit sits where chatCore already owns the decision, and the helpers themselves (nonStreamingProviderLeg.ts, serverOwnedToolLoopWire.ts) are under cap. Covered by tests/unit/chatcore-translation-paths.test.ts (72/74; the 2 open are issue #13043).", + "_rebaseline_2026_09_07_virtualfactory_crosses_the_new_file_cap": "open-sse/services/autoCombo/virtualFactory.ts crosses the 1200 new-file cap for the first time (1187 on the pre-wave tip, 1207 here). Growth is spread across the routing/free-tier wave, not one extractable block: #12794 feeds observed breaker state and model quality into snapshot scoring instead of neutral constants, #12792 adds the reliability factor the snapshot path was still ignoring and the pooled-latency bootstrap, and #12744 tightens the free-model predicate the factory consumes. FROZEN RATHER THAN SPLIT, deliberately, and this is debt: two cohesive extraction candidates are ready when someone owns the move — computeSnapshotWeights (~85 lines) and the credential-eligibility group hasUsableOAuthToken/hasProviderSpecificSessionData/isKeylessEligibleConnection/hasUsableConnectionCredential (~70 lines). Either alone clears 1200 from here. Splitting three contributors' just-merged work mid-batch was the larger risk." } diff --git a/docs/diagrams/auto-combo-scoring.mmd b/docs/diagrams/auto-combo-scoring.mmd index be1599df778..55d69cd9106 100644 --- a/docs/diagrams/auto-combo-scoring.mmd +++ b/docs/diagrams/auto-combo-scoring.mmd @@ -2,7 +2,7 @@ %% Reflects: open-sse/services/autoCombo/scoring.ts (DEFAULT_WEIGHTS, sum = 1.0) %% v3.8.50 %% svg-title: OmniRoute Auto-Combo 16-factor scoring -%% svg-description: Flow from an incoming request through eligible candidates, the 15 weighted scoring factors, descending score sort, top-N selection, and sequential dispatch. +%% svg-description: Flow from an incoming request through eligible candidates, the 16 weighted scoring factors, descending score sort, top-N selection, and sequential dispatch. flowchart TB Request["Incoming request"] --> Candidates["Eligible candidates
(provider × model × account)"] Candidates --> Score["Compute composite score
per candidate"] @@ -23,6 +23,7 @@ flowchart TB f13["resetWindowAffinity (0.0000)"] f14["connectionDensity (0.0476)"] f15["quality (0.0300)"] + f16["reliability (0.0000 DEFAULT, 0.03 packs, 0.04 reliable)"] end Score --> Factors diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index f6add5b77df..de18d72fc61 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 9dab70a26ba..163094ca6f6 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 ee99596179c..a47d4bfbd0f 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 4db6e639b57..f885fcd1da2 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 e073c3be2ca..3052a2a58f5 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 a234875f15a..5f2698ccce2 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 ddf7e03f64b..07e8e280c33 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 1039568356e..a4360f3bf61 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 d9808e84369..21af787e760 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 e4f7a201f3f..3a87b0baec2 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 93d66e64511..18afed8248d 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 5dc2b7929ec..4c9f23a6755 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 56bc6752f34..10407a2cf84 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 89c09b2fbbf..446406fb8ef 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 d0ec5591606..ea7d05f1c88 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 0b42b77310a..1cfc3581d91 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 4185cf36f13..4a67efe5c62 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 d2e76a9303c..99397ce9543 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 b1a40613285..f570044dc75 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 3a2aae0929c..969bed3557f 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 6c6dd286427..5c71dcf0115 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 03fd86a1002..9f9b10f2987 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 65760eca187..2f2136de903 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 49f5903147d..82339116d93 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 502816502c1..735e12f60ff 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 0b35be9e16c..9f5d7b811e4 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 a9199b8f7a0..c42a519c7a8 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 00511f60360..50e2977458e 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 fd44211ffc3..ef4d8160040 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 236aa182002..f8d0c756cef 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 fd167e45efb..df0be1494e8 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 e6cc4f9e5bf..71a26248b76 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 8b3c4567b29..75615c58685 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 bbe2b4fd6de..3f584cbb3fa 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 90c2ab16266..3e87b9b43e4 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 c432dc480fb..98b1f25cd84 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 9d6b0422ca5..a8ea8f92783 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 6d6c81eb2d2..b0dfc00aae9 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 1e3e334685d..d882bb74d47 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 decf982bcf7..a3fa116a381 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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 58ce9f5917b..84a1a6e5321 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 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/routing/AUTO-COMBO.md b/docs/routing/AUTO-COMBO.md index e3eaf7e1c52..2b406a7b2c3 100644 --- a/docs/routing/AUTO-COMBO.md +++ b/docs/routing/AUTO-COMBO.md @@ -184,7 +184,7 @@ See [#7992](https://github.com/diegosouzapw/OmniRoute/issues/7992) and [#7111](h ## How It Works (Persisted Auto-Combos) -The Auto-Combo Engine dynamically selects the best provider/model for each request using a **16-factor scoring function** (defined in `open-sse/services/autoCombo/scoring.ts` → `DEFAULT_WEIGHTS`). The default weights sum to `1.0`; custom weights are renormalized by `normalizeScoringWeights()`. Three of the sixteen — `cacheAffinity`, `resetWindowAffinity` and `reliability` — carry a default weight of `0`: they are still computed for every candidate, and `cacheAffinity` gates prompt-cache deduplication outside the score, so they are declared factors that simply do not vote by default. +The Auto-Combo Engine dynamically selects the best provider/model for each request using a **16-factor scoring function** (defined in `open-sse/services/autoCombo/scoring.ts` → `DEFAULT_WEIGHTS`). The default weights sum to `1.0`; custom weights are renormalized by `normalizeScoringWeights()`. Two of the sixteen — `cacheAffinity` and `resetWindowAffinity` — carry a default weight of `0`; `reliability` carries `0` in `DEFAULT_WEIGHTS` but `0.03` in generic packs and `0.04` in `reliability-first`, and `quality` carries `0.02` in packs (`0.03` in `quality-first`): they are still computed for every candidate, and `cacheAffinity` gates prompt-cache deduplication outside the score, so the zero-default factors simply do not vote by default while packs do. ![Auto-Combo 16-factor scoring](../diagrams/exported/auto-combo-scoring.svg) @@ -217,30 +217,32 @@ The Auto-Combo Engine dynamically selects the best provider/model for each reque | Factor | ship-fast | cost-saver | quality-first | offline-friendly | reliability-first | chaos-mode | | :-------------------- | :--------- | :--------- | :------------ | :--------------- | :---------------- | :--------- | -| `quota` | 0.1333 | 0.1333 | 0.0952 | **0.3524** | 0.1333 | 0.0476 | +| `quota` | 0.1133 | 0.1133 | 0.0752 | **0.3324** | 0.1133 | 0.0376 | | `health` | 0.2667 | 0.1810 | 0.1714 | 0.2667 | **0.3524** | **0.4000** | -| `costInv` | 0.0476 | **0.3524** | 0.0476 | 0.0952 | 0.0381 | 0.0190 | -| `latencyInv` | **0.3048** | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0286 | +| `costInv` | 0.0276 | **0.3324** | 0.0276 | 0.0752 | 0.0181 | 0.0140 | +| `latencyInv` | **0.3048** | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0186 | | `taskFit` | 0.0952 | 0.0952 | **0.3524** | 0.0000 | 0.0952 | 0.1905 | | `stability` | 0.0000 | 0.0476 | 0.1429 | 0.0952 | 0.1905 | 0.1714 | -| `tierPriority` | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0190 | +| `tierPriority` | 0.0376 | 0.0376 | 0.0276 | 0.0376 | 0.0276 | 0.0040 | | `tierAffinity` | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | | `specificityMatch` | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | -| `contextAffinity` | 0.0095 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0286 | +| `contextAffinity` | 0.0095 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0186 | | `sessionAvailability` | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0476 | | `resetWindowAffinity` | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0000 | | `connectionDensity` | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0476 | 0.0476 | +| `quality` | 0.02 | 0.02 | **0.03** | 0.02 | 0.02 | 0.02 | +| `reliability` | 0.03 | 0.03 | 0.03 | 0.03 | **0.04** | 0.03 | Notes: -- **No pack sets `quality`, and a pack replaces the weight map wholesale** (`weights = pack`, not a merge). `quality` carries `0.03` in `DEFAULT_WEIGHTS`, but under any mode pack it normalizes to `0` — selecting a pack silences the observed-quality signal completely. If you want quality feedback to influence routing, leave `modePack` unset and tune the weights directly. (`cacheAffinity` is also unset by every pack, but it defaults to `0` anyway, so nothing changes there.) +- **Packs carry `quality` and `reliability`** (`quality 0.02`, `quality-first 0.03`; `reliability 0.03`, `reliability-first 0.04`) and replace the weight map wholesale (`weights = pack`, not a merge). `DEFAULT_WEIGHTS` carries `quality 0.03 / reliability 0`; selecting `balanced`/`default` keeps those defaults, selecting a pack uses the pack's values above. On a cold pool (no observations yet, so `quality 0.5` and `reliability 1`) these two factors add `+0.04` under a generic pack (`0.03 + 0.01`), `+0.045` under `quality-first` and `+0.05` under `reliability-first`. - `tierAffinity`, `specificityMatch` and `resetWindowAffinity` are explicitly `0` in every pack. - Each pack's emphasis at a glance: - **ship-fast** → latencyInv 0.3048 + health 0.2667 (low-latency, healthy connections) - - **cost-saver** → costInv 0.3524 (cheapest tokens win) - - **quality-first** → taskFit 0.3524 + stability 0.1429 (best model for the task, consistent) - - **offline-friendly** → quota 0.3524 + health 0.2667 (max headroom regardless of speed/cost) - - **reliability-first** → health 0.3524 + stability 0.1905 (fewest surprises) + - **cost-saver** → costInv 0.3324 (cheapest tokens win) + - **quality-first** → taskFit 0.3524 + stability 0.1429 + quality 0.03, the highest of any pack (best model for the task, consistent) + - **offline-friendly** → quota 0.3324 + health 0.2667 (max headroom regardless of speed/cost) + - **reliability-first** → health 0.3524 + stability 0.1905 + reliability 0.04, the highest of any pack (fewest surprises) - **chaos-mode** → health 0.4000 + taskFit 0.1905 (fault-injection profile) ### Per-Request Controls (headers) — #6023 / #6024 / #6025 / #3470 diff --git a/llm.txt b/llm.txt index 2ebe387b797..a0977231f99 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, 171 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 172 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/ # 171 versioned SQL migration files +│ │ │ └── migrations/ # 172 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 171 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 172 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, 171 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, 172 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/open-sse/config/freeTierProviders.ts b/open-sse/config/freeTierProviders.ts new file mode 100644 index 00000000000..adf29ae5488 --- /dev/null +++ b/open-sse/config/freeTierProviders.ts @@ -0,0 +1,24 @@ +/** + * freeTierProviders.ts — registration list of providers with documented free models. + * + * Derived from `FREE_MODEL_BUDGETS` (`./freeModelCatalog.data.ts`) filtered by + * `grantsFreeAccess` (`./freeModelCatalog.ts`), so a `discontinued` entry never + * grants access. Pure data — no imports outside `open-sse/config`, no module + * state — so it cannot introduce a cycle and `open-sse/config/providerPluginManifest.ts` + * stays a light leaf. The open-sse typecheck gate forbids open-sse → src imports; + * `src/shared/utils/freeModels.ts` reads the same source for dashboard use. + */ + +import { FREE_MODEL_BUDGETS } from "./freeModelCatalog.data.ts"; +import { grantsFreeAccess } from "./freeModelCatalog.ts"; + +const FREE_BUDGETS = FREE_MODEL_BUDGETS.filter((m) => grantsFreeAccess(m.freeType)); + +export const FREE_TIER_PROVIDER_SET: ReadonlySet = new Set( + FREE_BUDGETS.map((m) => m.provider) +); + +export function hasFreeTierProvider(idOrAlias: string | undefined | null): boolean { + if (typeof idOrAlias !== "string") return false; + return FREE_TIER_PROVIDER_SET.has(idOrAlias); +} diff --git a/open-sse/config/providerPluginManifest.ts b/open-sse/config/providerPluginManifest.ts index 020803f4a3a..e97dfc020a8 100644 --- a/open-sse/config/providerPluginManifest.ts +++ b/open-sse/config/providerPluginManifest.ts @@ -1,10 +1,12 @@ import type { RegistryEntry, RegistryModel } from "./providers/shared.ts"; import { USAGE_FETCHER_PROVIDERS } from "../services/usage/fetcherProviders.ts"; import { USAGE_SUPPORTED_PROVIDERS } from "../services/usage/supportedProviders.ts"; +import { hasFreeTierProvider } from "./freeTierProviders.ts"; export type ProviderPluginCapability = | "apikey" | "custom-executor" + | "free-tier" | "oauth" | "passthrough-models" | "responses" @@ -156,6 +158,9 @@ function capabilitiesFor(entry: RegistryEntry, eligible: boolean): ProviderPlugi if (USAGE_SUPPORTED_PROVIDER_SET.has(entry.id)) { capabilities.add("usage-supported"); } + if ([entry.id, entry.alias].some(hasFreeTierProvider)) { + capabilities.add("free-tier"); + } return [...capabilities].sort(); } diff --git a/open-sse/services/__tests__/manifestAdapter.test.ts b/open-sse/services/__tests__/manifestAdapter.test.ts index e9430851a4c..56528887411 100644 --- a/open-sse/services/__tests__/manifestAdapter.test.ts +++ b/open-sse/services/__tests__/manifestAdapter.test.ts @@ -22,8 +22,8 @@ function makeTarget(provider: string, model: string): ResolvedComboTarget { describe("ManifestAdapter", () => { describe("generateRoutingHints - trivial query", () => { - it("returns prefer-free modifier for greeting", () => { - const hints = generateRoutingHints([], { + it("returns prefer-free modifier for greeting", async () => { + const hints = await generateRoutingHints([], { messages: [{ content: "Hello" }], }); expect(hints.strategyModifier).toBe("prefer-free"); @@ -32,8 +32,8 @@ describe("ManifestAdapter", () => { }); describe("generateRoutingHints - expert query", () => { - it("returns a valid modifier for complex input", () => { - const hints = generateRoutingHints([], { + it("returns a valid modifier for complex input", async () => { + const hints = await generateRoutingHints([], { messages: [ { content: @@ -47,25 +47,25 @@ describe("ManifestAdapter", () => { }); describe("generateRoutingHints - target classification", () => { - it("marks free provider as eligible for trivial query", () => { + it("marks free provider as eligible for trivial query", async () => { const targets = [makeTarget("kiro", "claude-sonnet-4.5")]; - const hints = generateRoutingHints(targets, { + const hints = await generateRoutingHints(targets, { messages: [{ content: "Hi" }], }); expect(hints.eligibleTargets.length).toBeGreaterThanOrEqual(0); }); - it("handles empty targets array gracefully", () => { - const hints = generateRoutingHints([], { + it("handles empty targets array gracefully", async () => { + const hints = await generateRoutingHints([], { messages: [{ content: "Hello" }], }); expect(hints.eligibleTargets.length).toBe(0); expect(hints.underqualifiedTargets.length).toBe(0); }); - it("classifies mixed targets for simple query", () => { + it("classifies mixed targets for simple query", async () => { const targets = [makeTarget("kiro", "claude-sonnet-4.5"), makeTarget("openai", "gpt-4o")]; - const hints = generateRoutingHints(targets, { + const hints = await generateRoutingHints(targets, { messages: [{ content: "Hello" }], }); expect(hints.eligibleTargets.length).toBeGreaterThanOrEqual(0); @@ -73,20 +73,20 @@ describe("ManifestAdapter", () => { }); describe("compareByCostEffectiveness", () => { - it("takes 3 arguments and returns a number", () => { + it("takes 3 arguments and returns a number", async () => { const a = makeTarget("deepseek", "deepseek-chat"); const b = makeTarget("openai", "gpt-4o"); - const hints = generateRoutingHints([a, b], { + const hints = await generateRoutingHints([a, b], { messages: [{ content: "Test" }], }); const result = compareByCostEffectiveness(a, b, hints); expect(typeof result).toBe("number"); }); - it("returns negative when a is cheaper than b", () => { + it("returns negative when a is cheaper than b", async () => { const a = makeTarget("deepseek", "deepseek-chat"); const b = makeTarget("openai", "gpt-4o"); - const hints = generateRoutingHints([a, b], { + const hints = await generateRoutingHints([a, b], { messages: [{ content: "Test" }], }); const result = compareByCostEffectiveness(a, b, hints); @@ -115,16 +115,16 @@ describe("ManifestAdapter", () => { }); describe("edge cases", () => { - it("handles empty targets array", () => { - const hints = generateRoutingHints([], { + it("handles empty targets array", async () => { + const hints = await generateRoutingHints([], { messages: [{ content: "Hello" }], }); expect(hints.eligibleTargets.length).toBe(0); expect(hints.underqualifiedTargets.length).toBe(0); }); - it("returns valid hints structure with no targets", () => { - const hints = generateRoutingHints([], { + it("returns valid hints structure with no targets", async () => { + const hints = await generateRoutingHints([], { messages: [{ content: "Test" }], }); expect("specificityLevel" in hints).toBe(true); diff --git a/open-sse/services/autoCombo/__tests__/autoCombo.test.ts b/open-sse/services/autoCombo/__tests__/autoCombo.test.ts index ba4cce5b838..e1821c002fd 100644 --- a/open-sse/services/autoCombo/__tests__/autoCombo.test.ts +++ b/open-sse/services/autoCombo/__tests__/autoCombo.test.ts @@ -2,7 +2,7 @@ * Unit tests for Auto-Combo Engine (Phase 5) */ -import { describe, it, expect, beforeEach, vi } from "vitest"; +import { describe, it, expect, beforeEach } from "vitest"; import { calculateFactors, calculateScore, DEFAULT_WEIGHTS, validateWeights } from "../scoring"; import type { ProviderCandidate, ScoringWeights } from "../scoring"; import { @@ -242,6 +242,115 @@ describe("Mode Packs", () => { it("undefined pack should return undefined", () => { expect(getModePack("nonexistent")).toBeUndefined(); }); + + it("every pack carries quality>0 and reliability>0 and sums to 0.9999", () => { + for (const name of getModePackNames()) { + const w = getModePack(name)!; + expect(Number(w.quality)).toBeGreaterThan(0); + expect(Number(w.reliability)).toBeGreaterThan(0); + const sum = Object.values(w).reduce((a, b) => a + Number(b), 0); + expect(sum).toBeCloseTo(0.9999, 3); + } + }); +}); + +describe("Mode pack ranking gates (cold/warm/health)", () => { + function scoreOne(candidate: ProviderCandidate, pack: ScoringWeights): number { + const factors = calculateFactors(candidate, [candidate], "coding", () => 0.5); + return calculateScore(factors, pack); + } + it("cold pool ranking unchanged (reliability 1, quality 0.5 neutrals)", () => { + const a: ProviderCandidate = { + circuitBreakerState: "CLOSED", + failureRate: undefined, + quality: undefined, + quotaRemaining: 50, + quotaTotal: 100, + costPer1MTokens: 1, + p95LatencyMs: 100, + accountTier: "pro", + latencyStdDev: 10, + }; + const b: ProviderCandidate = { + circuitBreakerState: "CLOSED", + failureRate: undefined, + quality: undefined, + quotaRemaining: 50, + quotaTotal: 100, + costPer1MTokens: 1, + p95LatencyMs: 100, + accountTier: "pro", + latencyStdDev: 10, + }; + const pack = MODE_PACKS["reliability-first"]; + expect(scoreOne(a, pack)).toBeCloseTo(scoreOne(b, pack), 5); + }); + it("warm reliability 0.01 vs 0.4 flips winner at health tie", () => { + const highFail: ProviderCandidate = { + circuitBreakerState: "CLOSED", + failureRate: 0.4, + quality: 0.5, + quotaRemaining: 50, + quotaTotal: 100, + costPer1MTokens: 1, + p95LatencyMs: 100, + accountTier: "pro", + latencyStdDev: 10, + }; + const lowFail: ProviderCandidate = { + circuitBreakerState: "CLOSED", + failureRate: 0.01, + quality: 0.5, + quotaRemaining: 50, + quotaTotal: 100, + costPer1MTokens: 1, + p95LatencyMs: 100, + accountTier: "pro", + latencyStdDev: 10, + }; + const pack = MODE_PACKS["reliability-first"]; + expect(scoreOne(lowFail, pack)).toBeGreaterThan(scoreOne(highFail, pack)); + }); + it("boundedRate NaN yields reliability 1", () => { + const c: ProviderCandidate = { + circuitBreakerState: "CLOSED", + failureRate: NaN, + quotaRemaining: 50, + quotaTotal: 100, + costPer1MTokens: 1, + p95LatencyMs: 100, + accountTier: "pro", + latencyStdDev: 10, + }; + const factors = calculateFactors(c, [c], "coding", () => 0.5); + expect(factors.reliability).toBe(1); + }); + it("health CLOSED vs HALF_OPEN still outweighs reliability gap", () => { + const healthyHighFail: ProviderCandidate = { + circuitBreakerState: "CLOSED", + failureRate: 0.4, + quality: 0.5, + quotaRemaining: 50, + quotaTotal: 100, + costPer1MTokens: 1, + p95LatencyMs: 100, + accountTier: "pro", + latencyStdDev: 10, + }; + const halfOpenLowFail: ProviderCandidate = { + circuitBreakerState: "HALF_OPEN", + failureRate: 0.01, + quality: 0.5, + quotaRemaining: 50, + quotaTotal: 100, + costPer1MTokens: 1, + p95LatencyMs: 100, + accountTier: "pro", + latencyStdDev: 10, + }; + const pack = MODE_PACKS["reliability-first"]; + expect(scoreOne(healthyHighFail, pack)).toBeGreaterThan(scoreOne(halfOpenLowFail, pack)); + }); }); describe("SLA-aware Strategy", () => { diff --git a/open-sse/services/autoCombo/complexityRouter.ts b/open-sse/services/autoCombo/complexityRouter.ts index 602fb1450bd..5b49adf4833 100644 --- a/open-sse/services/autoCombo/complexityRouter.ts +++ b/open-sse/services/autoCombo/complexityRouter.ts @@ -75,11 +75,11 @@ export function classifyRequestComplexity(input: RuleInput): ComplexityClassific * failure — fail-open, so scoring stays tier-neutral. Extracted from combo.ts to * keep the complexity-routing logic in one module. */ -export function buildComplexityRoutingHint( +export async function buildComplexityRoutingHint( modelTargets: Parameters[0], body: { messages?: unknown; tools?: unknown; model?: unknown } | null | undefined, log: { info: (tag: string, message: string) => void } -): RoutingHint | null { +): Promise { try { const ruleInput = { messages: Array.isArray(body?.messages) @@ -92,7 +92,7 @@ export function buildComplexityRoutingHint( : undefined, model: typeof body?.model === "string" ? body.model : undefined, }; - const hint = generateRoutingHints(modelTargets, ruleInput); + const hint = await generateRoutingHints(modelTargets, ruleInput); // Tool-use escalation: floor the recommended tier at "cheap" so scoring // favors function-calling-reliable models for agentic requests. const classification = classifyRequestComplexity(ruleInput); diff --git a/open-sse/services/autoCombo/engine.ts b/open-sse/services/autoCombo/engine.ts index 9e91552f36d..07a94e2dc47 100644 --- a/open-sse/services/autoCombo/engine.ts +++ b/open-sse/services/autoCombo/engine.ts @@ -39,6 +39,7 @@ export interface AutoComboConfig { * silently overspending. */ budgetFallback?: "cheapest" | "strict"; + estimatedInputTokens?: number; // tokens the budget is computed against (default 1000) explorationRate: number; // 0.05 = 5% exploratory /** If set, RouterStrategy name to use for selection ('rules' | 'cost' | 'latency') */ routerStrategy?: string; @@ -311,9 +312,13 @@ export function selectProvider( for (const c of candidates) { costMap.set(`${c.provider}\0${c.model}`, c.costPer1MTokens); } + const estimatedTokens = + Number.isFinite(config.estimatedInputTokens) && config.estimatedInputTokens! > 0 + ? config.estimatedInputTokens! + : 1000; const estimatedCostFor = (s: ScoredProvider) => { const cost = costMap.get(`${s.provider}\0${s.model}`) ?? 0; - return (cost / 1_000_000) * 1000; + return (cost / 1_000_000) * estimatedTokens; }; if (estimatedCostFor(selected) > config.budgetCap) { const budgetOk = candidates_.filter((s) => estimatedCostFor(s) <= config.budgetCap!); diff --git a/open-sse/services/autoCombo/modePacks.ts b/open-sse/services/autoCombo/modePacks.ts index 267dfceeec0..4c725a0b8c8 100644 --- a/open-sse/services/autoCombo/modePacks.ts +++ b/open-sse/services/autoCombo/modePacks.ts @@ -4,8 +4,10 @@ * Each pack optimizes for a different priority: * - ship-fast: Prioritize latency and health * - cost-saver: Prioritize cost efficiency - * - quality-first: Prioritize task fitness and stability + * - quality-first: Prioritize task fitness and stability (highest quality weight, 0.03) * - offline-friendly: Prioritize quota availability + * - reliability-first: Prioritize health+stability (highest reliability weight, 0.04) + * - chaos-mode: Fault-injection — health > stability > taskFit */ import type { ScoringWeights } from "./scoring"; @@ -14,85 +16,95 @@ export const MODE_PACKS: Record = { // Prioritize latency → health. tierPriority replaces 0.05 from stability. // tierAffinity/specificityMatch stay at 0 (manifest-routing-only weights). "ship-fast": { - quota: 0.1333, + quota: 0.1133, health: 0.2667, - costInv: 0.0476, + costInv: 0.0276, latencyInv: 0.3048, taskFit: 0.0952, stability: 0, - tierPriority: 0.0476, + tierPriority: 0.0376, tierAffinity: 0, specificityMatch: 0, contextAffinity: 0.0095, sessionAvailability: 0.0476, resetWindowAffinity: 0, connectionDensity: 0.0476, + quality: 0.02, + reliability: 0.03, }, // Prioritize cost. tierPriority replaces 0.05 from stability. "cost-saver": { - quota: 0.1333, + quota: 0.1133, health: 0.181, - costInv: 0.3524, + costInv: 0.3324, latencyInv: 0.0476, taskFit: 0.0952, stability: 0.0476, - tierPriority: 0.0476, + tierPriority: 0.0376, tierAffinity: 0, specificityMatch: 0, contextAffinity: 0, sessionAvailability: 0.0476, resetWindowAffinity: 0, connectionDensity: 0.0476, + quality: 0.02, + reliability: 0.03, }, // Prioritize task fitness. tierPriority replaces 0.05 from latencyInv. "quality-first": { - quota: 0.0952, + quota: 0.0752, health: 0.1714, - costInv: 0.0476, + costInv: 0.0276, latencyInv: 0.0476, taskFit: 0.3524, stability: 0.1429, - tierPriority: 0.0476, + tierPriority: 0.0276, tierAffinity: 0, specificityMatch: 0, contextAffinity: 0, sessionAvailability: 0.0476, resetWindowAffinity: 0, connectionDensity: 0.0476, + quality: 0.03, + reliability: 0.03, }, // Prioritize quota availability. tierPriority replaces 0.05 from taskFit. "offline-friendly": { - quota: 0.3524, + quota: 0.3324, health: 0.2667, - costInv: 0.0952, + costInv: 0.0752, latencyInv: 0.0476, taskFit: 0, stability: 0.0952, - tierPriority: 0.0476, + tierPriority: 0.0376, tierAffinity: 0, specificityMatch: 0, contextAffinity: 0, sessionAvailability: 0.0476, resetWindowAffinity: 0, connectionDensity: 0.0476, + quality: 0.02, + reliability: 0.03, }, // #4235 `:reliable` — prioritize healthy, low-variance providers (high availability). // health (circuit-breaker) + stability (latency std-dev) dominate; weights sum to ~1.0 // (re-normalized after #8940 added sessionAvailability without rebalancing — #9985). "reliability-first": { - quota: 0.1333, + quota: 0.1133, health: 0.3524, - costInv: 0.0381, + costInv: 0.0181, latencyInv: 0.0476, taskFit: 0.0952, stability: 0.1905, - tierPriority: 0.0476, + tierPriority: 0.0276, tierAffinity: 0, specificityMatch: 0, contextAffinity: 0, sessionAvailability: 0.0476, resetWindowAffinity: 0, connectionDensity: 0.0476, + quality: 0.02, + reliability: 0.04, }, // Chaos mode — priority: health > stability > taskFit > latency. // Selects top-N healthy providers for parallel dispatch. Favors providers with @@ -101,19 +113,21 @@ export const MODE_PACKS: Record = { // to picking the most stable providers); connectionDensity boosted slightly to // prefer providers with multiple accounts (more resilient to per-account rate limits). "chaos-mode": { - quota: 0.0476, + quota: 0.0376, health: 0.4, - costInv: 0.019, - latencyInv: 0.0286, + costInv: 0.014, + latencyInv: 0.0186, taskFit: 0.1905, stability: 0.1714, - tierPriority: 0.019, + tierPriority: 0.004, tierAffinity: 0, specificityMatch: 0, - contextAffinity: 0.0286, + contextAffinity: 0.0186, sessionAvailability: 0.0476, resetWindowAffinity: 0, connectionDensity: 0.0476, + quality: 0.02, + reliability: 0.03, }, }; diff --git a/open-sse/services/autoCombo/paidModelFilter.ts b/open-sse/services/autoCombo/paidModelFilter.ts index d50014021ce..cc9e82e0508 100644 --- a/open-sse/services/autoCombo/paidModelFilter.ts +++ b/open-sse/services/autoCombo/paidModelFilter.ts @@ -11,7 +11,7 @@ * Kept as a pure, dependency-light function so the filter is unit-testable in * isolation without seeding the DB-backed virtual factory. */ -import { isFreeModel, providerHasFreeModels } from "@/shared/utils/freeModels"; +import { isFreeForProvider } from "@/shared/utils/freeModels"; interface PaidFilterCandidate { provider: string; @@ -22,10 +22,7 @@ interface PaidFilterCandidate { * selected model itself qualifies as free — mirrors `shouldHidePaid` in * `src/app/api/v1/models/catalog.ts`. */ function isFreeCandidate(candidate: PaidFilterCandidate): boolean { - return ( - providerHasFreeModels(candidate.provider) && - isFreeModel(candidate.provider, { id: candidate.model }) - ); + return isFreeForProvider(candidate.provider, { id: candidate.model }); } /** diff --git a/open-sse/services/autoCombo/scoring.ts b/open-sse/services/autoCombo/scoring.ts index 2173d41f068..a9977ccba55 100644 --- a/open-sse/services/autoCombo/scoring.ts +++ b/open-sse/services/autoCombo/scoring.ts @@ -78,11 +78,12 @@ export const DEFAULT_WEIGHTS: ScoringWeights = { // the new quality signal (observed output quality over time) gets a real, // if smaller, vote. Sum remains exactly 1.0. quality: 0.03, - // Declared but silent, like `cacheAffinity` and `resetWindowAffinity`: every - // candidate already carries a measured failure rate (24h of usage history - // behind a ten-sample floor, real-time metrics otherwise) and the scorer had - // no way to read it. Which weight it deserves is a product call backed by - // measurement, so this ships at 0 and leaves the ranking exactly as it was. + // Declared but silent in DEFAULT (like `cacheAffinity`/`resetWindowAffinity`): + // every candidate already carries a measured failure rate (24h of usage + // history behind a ten-sample floor, real-time metrics otherwise) and the + // scorer had no way to read it. Which weight it deserves is a product call + // backed by measurement, so DEFAULT ships at 0 and leaves the ranking as + // it was; `reliability-first` ships at 0.04 and generic packs at 0.03. reliability: 0, }; @@ -207,6 +208,19 @@ export function calculateTierScore( return Math.min(1, baseScore * 0.8 + resetBonus * 0.2); } +/** + * Project the account tier from a provider connection row. + * Tier whitelist in ONE place: test and combo.ts import this, never mirror it. + */ +export function projectAccountTier( + row: Record | undefined +): "ultra" | "pro" | "standard" | "free" | undefined { + const psd = row?.providerSpecificData as Record | undefined; + const raw = row?.accountTier ?? psd?.accountTier; + const v = typeof raw === "string" ? raw.toLowerCase() : undefined; + return v === "ultra" || v === "pro" || v === "standard" || v === "free" ? v : undefined; +} + function calculateTierAffinity( candidate: ProviderCandidate, hint: RoutingHint | undefined | null @@ -279,6 +293,14 @@ export function computePoolMaxima(pool: ProviderCandidate[]): PoolMaxima { * `speedRanking.ts` so both consumers of the same signal agree, including on * garbage input. */ +/** Reliability factor from an observed failure (else error) rate; absent reads fully reliable. */ +export function reliabilityFactor(candidate: { + failureRate?: number | null; + errorRate?: number | null; +}): number { + return clamp01(1 - boundedRate(candidate.failureRate ?? candidate.errorRate)); +} + function boundedRate(value: number | null | undefined): number { if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 0; return Math.min(1, value); @@ -327,7 +349,7 @@ export function calculateFactors( // bounded BEFORE the subtraction, exactly as `toBoundedRate` does there -- // `clamp01(1 - NaN)` would be 0, i.e. "fails every call", which is the // opposite of what corrupt telemetry should mean. - reliability: clamp01(1 - boundedRate(candidate.failureRate ?? candidate.errorRate)), + reliability: reliabilityFactor(candidate), }; } diff --git a/open-sse/services/autoCombo/snapshotBreaker.ts b/open-sse/services/autoCombo/snapshotBreaker.ts new file mode 100644 index 00000000000..7af1814bf8d --- /dev/null +++ b/open-sse/services/autoCombo/snapshotBreaker.ts @@ -0,0 +1,31 @@ +import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker.ts"; + +export type BreakerState = "CLOSED" | "HALF_OPEN" | "OPEN"; + +/** + * Build-time breaker state per provider. Any other state (e.g. DEGRADED) and a + * failed lookup leave the provider absent, so it scores neutral. + */ +export function readBreakerStates(providers: Iterable): Map { + const states = new Map(); + for (const provider of providers) { + try { + const state = getCircuitBreaker(provider).getStatus().state as string; + if (state === "OPEN" || state === "HALF_OPEN" || state === "CLOSED") { + states.set(provider, state); + } + } catch { + // An unreadable breaker leaves the provider neutral. + } + } + return states; +} + +/** Snapshot health factor: OPEN 0, CLOSED 1, HALF_OPEN or unknown 0.5 (neutral). */ +export function snapshotHealthFactor( + states: ReadonlyMap | undefined, + provider: string +): number { + const state = states?.get(provider); + return state === "OPEN" ? 0 : state === "CLOSED" ? 1 : 0.5; +} diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts index 8471727a9b7..51f0be66069 100644 --- a/open-sse/services/autoCombo/virtualFactory.ts +++ b/open-sse/services/autoCombo/virtualFactory.ts @@ -1,6 +1,6 @@ import { AutoComboConfig } from "./engine"; import { MODE_PACKS } from "./modePacks"; -import { DEFAULT_WEIGHTS, ScoringWeights } from "./scoring"; +import { DEFAULT_WEIGHTS, reliabilityFactor, ScoringWeights } from "./scoring"; import { getCachedProviderConnections } from "@/lib/db/readCache"; import { getSettings } from "@/lib/db/settings"; import { getProviderRegistry } from "./providerRegistryAccessor"; @@ -26,6 +26,9 @@ import { type AutoTier, } from "./suffixComposition"; import { classifyTier } from "../tierResolver"; +import { getQualityScore } from "../routing/quality.ts"; +import { readBreakerStates, snapshotHealthFactor, type BreakerState } from "./snapshotBreaker.ts"; +import { resolveVirtualCost } from "../providerCostData"; import type { AutoVariant } from "./autoPrefix"; import { buildFamilyCandidateFilter, type ModelFamily } from "./modelFamily"; import { getHiddenModelsByProvider } from "@/models"; @@ -105,12 +108,16 @@ export interface VirtualAutoComboCandidate { model: string; modelStr: string; // e.g., 'openai/gpt-4o' costPer1MTokens: number; // from providerRegistry + /** Observed failure rate 0..1 when known; null/absent reads fully reliable. */ + failureRate?: number | null; /** Build-local capability snapshot. Runtime calls rebuild it; catalog entries reuse it. */ resolvedContextLength?: number | null; resolvedMaxOutputTokens?: number | null; resolvedSupportsVision?: boolean; resolvedReasoning?: boolean; resolvedSupportsThinking?: boolean; + /** Observed feedback quality 0..1 (the same signal as ProviderCandidate.quality). */ + quality?: number | null; /** * Why STRICT_ZERO_COST would exclude this candidate, or null when it would * not. Only populated for the read-only inspector build (`skip`), where the @@ -440,7 +447,7 @@ function getNoAuthCandidates( connectionId: SYNTHETIC_NOAUTH_CONNECTION_ID, model: modelId, modelStr: `${routingPrefix}/${modelId}`, - costPer1MTokens: 0, + costPer1MTokens: resolveVirtualCost(providerId, modelId), }); } } @@ -544,7 +551,7 @@ function yieldVirtualAutoPreparationTurn(): Promise { return new Promise((resolve) => setImmediate(resolve)); } -async function attachPreparedCapabilityValues( +export async function attachPreparedCapabilityValues( candidates: readonly VirtualAutoComboCandidate[], state: PreparedCapabilityState ): Promise { @@ -591,7 +598,11 @@ async function attachPreparedCapabilityValues( await yieldVirtualAutoPreparationTurn(); } } - prepared.push({ ...candidate, ...values }); + prepared.push({ + ...candidate, + ...values, + quality: getQualityScore(candidate.provider, candidate.model), + }); } return prepared; } @@ -718,7 +729,7 @@ export async function prepareVirtualAutoComboInputs( allowedConnectionIds, model: modelId, modelStr: `${providerId}/${modelId}`, - costPer1MTokens: 0, // Not used in virtual auto-combo (LKGP uses session stickiness) + costPer1MTokens: resolveVirtualCost(providerId, modelId), }); } } @@ -846,7 +857,8 @@ export async function prepareVirtualAutoComboInputs( */ export function computeSnapshotWeights( candidates: readonly VirtualAutoComboCandidate[], - weights: ScoringWeights + weights: ScoringWeights, + breakerByProvider?: ReadonlyMap ): Map { const scores = new Map(); for (const c of candidates) { @@ -884,8 +896,14 @@ export function computeSnapshotWeights( // (no runtime data at snapshot time, so equal baseline) if (weights.latencyInv > 0) score += weights.latencyInv * 0.5; - // health + quota: no runtime telemetry at snapshot time → neutral baseline - score += (weights.health + weights.quota) * 0.5; + // reliability (#12792): the same failure-rate factor as scoring.ts; absent reads as + // fully reliable. The snapshot path was the only one still ignoring it. + if (weights.reliability > 0) score += weights.reliability * reliabilityFactor(c); + + // health: build-time breaker state (OPEN 0, CLOSED 1, else neutral 0.5); quota stays neutral + score += weights.health * snapshotHealthFactor(breakerByProvider, c.provider); + score += weights.quota * 0.5; + score += (weights.quality ?? 0) * (Number.isFinite(c.quality) ? Number(c.quality) : 0.5); scores.set(c.modelStr, Math.min(score, 1)); } @@ -1086,7 +1104,8 @@ export async function createVirtualAutoComboFromPrepared( } const providerPool = [...new Set(effectivePool.map((c) => c.provider))]; - const snapshotScores = computeSnapshotWeights(effectivePool, weights); + const breakerStates = readBreakerStates(providerPool); + const snapshotScores = computeSnapshotWeights(effectivePool, weights, breakerStates); const models = effectivePool.map((candidate, index) => ({ id: `virtual-auto-${variant || "default"}-${index + 1}-${candidate.provider}`, kind: "model" as const, diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 785a543382b..acc72de4ae8 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -33,7 +33,7 @@ import { rejectRetiredAutoComboCandidates } from "./modelLifecycle.ts"; import { createComboContext } from "./combo/context.ts"; import { phaseComboSetup } from "./combo/comboSetup.ts"; -import { type ProviderCandidate } from "./autoCombo/scoring.ts"; +import { projectAccountTier, type ProviderCandidate } from "./autoCombo/scoring.ts"; import { getSessionConnection } from "./sessionManager.ts"; import { getOAuthSessionAvailability } from "./oauthSessionOccupancy.ts"; @@ -251,6 +251,63 @@ function getBootstrapLatencyMs(modelId: string): number { return DEFAULT_MODEL_P95_MS[normalized] ?? 1500; } +export function poolMedianP95Ms( + stats: Record +): number | undefined { + const vals = Object.values(stats) + .map((st) => Number(st?.p95LatencyMs)) + .filter((v) => Number.isFinite(v) && v > 0) + .sort((a, b) => a - b); + return vals.length ? vals[(vals.length - 1) >> 1] : undefined; +} + +const BOOTSTRAP_WARN_WINDOW_MS = 3600_000; +export let bootstrapLatencyHits = 0; // exported for testability (reset in tests) +export let bootstrapLatencyTotal = 0; +let bootstrapWarnedAt = 0; +export function resetBootstrapCounters(): void { + bootstrapLatencyHits = 0; + bootstrapLatencyTotal = 0; + bootstrapWarnedAt = 0; +} +export function bootstrapMs(model: string, poolMedian: number | undefined): number { + bootstrapLatencyTotal++; + const table = DEFAULT_MODEL_P95_MS[String(model || "").toLowerCase()]; + if (table !== undefined) return table; + bootstrapLatencyHits++; + return poolMedian ?? 1500; +} + +// Pure and testable without timers: the throttled 1h warn + cold-start exemption live here. +export function shouldWarnBootstrap( + hits: number, + total: number, + hasStats: boolean, + now: number, + lastWarn: number +): boolean { + if (!hasStats || total === 0) return false; + if (hits / total <= 0.3) return false; + return now - lastWarn >= BOOTSTRAP_WARN_WINDOW_MS; +} + +function maybeWarnBootstrapDominant(hasStats: boolean): void { + if ( + !shouldWarnBootstrap( + bootstrapLatencyHits, + bootstrapLatencyTotal, + hasStats, + Date.now(), + bootstrapWarnedAt + ) + ) + return; + bootstrapWarnedAt = Date.now(); + console.warn( + `[combo] bootstrap latency dominant (${bootstrapLatencyHits}/${bootstrapLatencyTotal}) — scoring runs on guesses` + ); +} + export async function buildAutoCandidates( targets: ResolvedComboTarget[], comboName: string, @@ -278,6 +335,8 @@ export async function buildAutoCandidates( } catch { // keep empty stats — auto-combo will use runtime + bootstrap signals } + const poolMedian = poolMedianP95Ms(historicalLatencyStats); + const hasStats = Object.keys(historicalLatencyStats).length > 0; const uniqueProviders = Array.from( new Set( @@ -364,10 +423,10 @@ export async function buildAutoCandidates( const p95LatencyMs = hasHistoricalSignal ? Number.isFinite(historicalP95Latency) && historicalP95Latency > 0 ? historicalP95Latency - : getBootstrapLatencyMs(model) + : bootstrapMs(model, poolMedian) : Number.isFinite(avgLatency) && avgLatency > 0 ? avgLatency - : getBootstrapLatencyMs(model); + : bootstrapMs(model, poolMedian); const errorRate = hasHistoricalSignal ? Number.isFinite(historicalSuccessRate) && @@ -488,8 +547,15 @@ export async function buildAutoCandidates( latencyStdDev, errorRate, ...speedTelemetry, - accountTier: "standard" as const, - quotaResetIntervalSecs: 86400, + accountTier: projectAccountTier(connection as Record | undefined), + quotaResetIntervalSecs: (() => { + const tierConn = connection as Record | undefined; + const tierPsd = tierConn?.providerSpecificData as Record | undefined; + const rawInterval = tierConn?.quotaResetIntervalSecs ?? tierPsd?.quotaResetIntervalSecs; + return typeof rawInterval === "number" && Number.isFinite(rawInterval) && rawInterval > 0 + ? rawInterval + : 86400; + })(), contextAffinity, sessionAvailability, resetWindowAffinity, @@ -509,6 +575,7 @@ export async function buildAutoCandidates( // Filter out candidates whose model is hidden by the user in the dashboard, // then drop vendor-retired ids so auto-combo cannot pick them (#11625). + maybeWarnBootstrapDominant(hasStats); return rejectRetiredAutoComboCandidates( candidates.filter((c) => { const hiddenModels = hiddenModelsMap.get(c.provider); diff --git a/open-sse/services/combo/applyStrategyOrdering.ts b/open-sse/services/combo/applyStrategyOrdering.ts index 405b0428632..a0dc11ebc03 100644 --- a/open-sse/services/combo/applyStrategyOrdering.ts +++ b/open-sse/services/combo/applyStrategyOrdering.ts @@ -157,7 +157,7 @@ export async function applyStrategyOrdering( orderedTargets = await sortTargetsByCost(orderedTargets); if (config.manifestRouting === true) { try { - const manifestHint = generateRoutingHints( + const manifestHint = await generateRoutingHints( orderedTargets.filter((t) => t.kind === "model"), { messages: Array.isArray(body?.messages) diff --git a/open-sse/services/combo/resolveAutoStrategy.ts b/open-sse/services/combo/resolveAutoStrategy.ts index 9882f2da4e2..134f8100b64 100644 --- a/open-sse/services/combo/resolveAutoStrategy.ts +++ b/open-sse/services/combo/resolveAutoStrategy.ts @@ -315,7 +315,7 @@ export async function resolveAutoStrategyOrder( // specificityMatch favor candidates whose tier matches the request. const autoManifestHint: RoutingHint | null = config.complexityAwareRouting === true - ? buildComplexityRoutingHint( + ? await buildComplexityRoutingHint( eligibleTargets.filter((t) => t.kind === "model"), body, log @@ -408,6 +408,7 @@ export async function resolveAutoStrategyOrder( modePack, budgetCap, budgetFallback, + estimatedInputTokens, explorationRate, }, routableCandidates, diff --git a/open-sse/services/manifestAdapter.ts b/open-sse/services/manifestAdapter.ts index a0528e48be5..5ca43aa78a6 100644 --- a/open-sse/services/manifestAdapter.ts +++ b/open-sse/services/manifestAdapter.ts @@ -1,7 +1,7 @@ import type { TierAssignment, ProviderTier } from "./tierTypes"; import { PROVIDER_TIER } from "./tierTypes"; import type { SpecificityResult, SpecificityLevel } from "./specificityTypes"; -import { classifyTier } from "./tierResolver"; +import { classifyTier, classifyTierAsync } from "./tierResolver"; import { analyzeSpecificity, getSpecificityLevel, @@ -29,17 +29,24 @@ export interface RoutingHint { strategyModifier: StrategyModifier; } -export function generateRoutingHints( +export async function generateRoutingHints( targets: ResolvedComboTarget[], input: RuleInput -): RoutingHint { +): Promise { const tierAssignments = new Map(); - for (const target of targets) { - if (target.kind !== "model") continue; - const key = `${target.provider}::${target.modelStr}`; - if (!tierAssignments.has(key)) { - tierAssignments.set(key, classifyTier(target.provider, target.modelStr)); - } + const modelTargets = targets.filter((t) => t.kind === "model"); + const settled = await Promise.all( + modelTargets.map(async (target) => { + const key = `${target.provider}::${target.modelStr}`; + try { + return [key, await classifyTierAsync(target.provider, target.modelStr)] as const; + } catch { + return [key, classifyTier(target.provider, target.modelStr)] as const; + } + }) + ); + for (const [key, assignment] of settled) { + if (!tierAssignments.has(key)) tierAssignments.set(key, assignment); } const specificity = analyzeSpecificity(input); diff --git a/open-sse/services/providerCostData.ts b/open-sse/services/providerCostData.ts index 8464635c921..675514d816e 100644 --- a/open-sse/services/providerCostData.ts +++ b/open-sse/services/providerCostData.ts @@ -1,4 +1,5 @@ import { getPricingForModel as getDefaultPricingForModel } from "@/shared/constants/pricing"; +import { isFreeModel } from "@/shared/utils/freeModels"; import type { TierConfig } from "./tierTypes"; export interface ModelPricing { @@ -38,14 +39,16 @@ export const KNOWN_MODEL_PRICING: Record = { }; export function getModelPricing(provider: string, model: string): ModelPricing { - const providerKey = `${provider}/${model}`.toLowerCase(); - if (KNOWN_MODEL_PRICING[providerKey]) { - return KNOWN_MODEL_PRICING[providerKey]; - } - const providerPricing = getDefaultPricingForModel(provider, model); - if (providerPricing) { - const inputCostPer1M = Number(providerPricing.input); - const outputCostPer1M = Number(providerPricing.output); + const normalized = String(model || "") + .split("/") + .pop()! + .toLowerCase(); + const providerHit = KNOWN_MODEL_PRICING[`${provider}/${normalized}`.toLowerCase()]; + if (providerHit) return providerHit; + const defaultPricing = getDefaultPricingForModel(provider, model); + if (defaultPricing) { + const inputCostPer1M = Number(defaultPricing.input); + const outputCostPer1M = Number(defaultPricing.output); if (Number.isFinite(inputCostPer1M) && Number.isFinite(outputCostPer1M)) { return { inputCostPer1M, @@ -54,13 +57,18 @@ export function getModelPricing(provider: string, model: string): ModelPricing { }; } } - const directKey = model.toLowerCase(); - if (KNOWN_MODEL_PRICING[directKey]) { - return KNOWN_MODEL_PRICING[directKey]; - } + const genericHit = KNOWN_MODEL_PRICING[normalized]; + if (genericHit) return genericHit; + if (isFreeModel(provider, { id: normalized })) + return { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true }; return { inputCostPer1M: 5.0, outputCostPer1M: 15.0, isFree: false }; } +/** Input cost per 1M tokens a virtual auto-combo candidate is scored at. */ +export function resolveVirtualCost(providerId: string, modelId: string): number { + return getModelPricing(providerId, modelId).inputCostPer1M; +} + export function isExplicitlyFree(provider: string, config: TierConfig): boolean { return config.freeProviders.includes(provider.toLowerCase()); } diff --git a/open-sse/services/tierResolver.ts b/open-sse/services/tierResolver.ts index b1702c5d9c7..d83e43bef03 100644 --- a/open-sse/services/tierResolver.ts +++ b/open-sse/services/tierResolver.ts @@ -142,3 +142,40 @@ export function getTierStats(): Record { } return stats; } + +export let tierAsyncFallbackTotal = 0; // exported for testability + +export async function classifyTierAsync(provider: string, model: string): Promise { + try { + const { getPricingForModel } = await import("@/lib/db/settings"); + const db = await getPricingForModel(provider, model); + const input = Number((db as { input?: unknown } | null)?.input); + const output = Number((db as { output?: unknown } | null)?.output); + if (Number.isFinite(input) && input >= 0) { + const out = Number.isFinite(output) && output >= 0 ? output : input; + // Same thresholds as the sync path below; a DB $0 lands FREE by threshold + // (the DB carries no isFree flag of its own). + let tier: ProviderTier; + if (input <= currentConfig.defaults.freeThreshold) tier = PROVIDER_TIER.FREE; + else if (input <= currentConfig.defaults.cheapThreshold) tier = PROVIDER_TIER.CHEAP; + else tier = PROVIDER_TIER.PREMIUM; + const sync = getModelPricing(provider, model); // keeps freeQuotaLimit when DB is mute + const assignment: TierAssignment = { + provider, + model, + tier, + reason: `DB cost-based: $${input}/M input`, + costPer1MInput: input, + costPer1MOutput: out, + hasFreeTier: input === 0, + freeQuotaLimit: sync.freeQuotaLimit, + }; + tierCache.set(cacheKey(provider, model), assignment); + return assignment; + } + } catch { + // fall through to sync + } + tierAsyncFallbackTotal++; + return classifyTier(provider, model); +} diff --git a/package.json b/package.json index 5cbcf71b42b..e8f33893543 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,7 @@ "check:model-lifecycle": "node --import tsx/esm scripts/check/check-model-lifecycle.mjs", "check:provider-consistency": "bun scripts/check/check-provider-consistency.ts", "check:provider-assets": "node scripts/check/check-provider-assets.mjs", + "check:provider-order-sync": "node scripts/check/check-provider-order-sync.mjs", "check:provider-asset-provenance": "node scripts/check/check-provider-asset-provenance.mjs", "check:nvidia-catalog-drift": "node --import tsx/esm scripts/check/check-nvidia-catalog-drift.ts", "check:fetch-targets": "node scripts/check/check-fetch-targets.mjs", @@ -264,6 +265,7 @@ "coverage:report": "cross-env NODE_OPTIONS=--max-old-space-size=8192 c8 report --merge-async --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov", "coverage:summary": "node scripts/check/test-report-summary.mjs --input coverage/coverage-summary.json --output coverage/coverage-report.md", "check:pr-test-policy": "node scripts/check/check-pr-test-policy.mjs", + "check:pricing-freshness": "node scripts/check/check-pricing-freshness.mjs", "coverage:report:legacy": "c8 report --output-dir coverage --exclude=open-sse --reporter=text --reporter=text-summary", "test:all": "npm run test:unit && npm run test:vitest && npm run test:vitest:ui && npm run test:ecosystem && npm run test:e2e", "check": "npm run lint && npm run test", diff --git a/scripts/check/check-docs-counts-sync.mjs b/scripts/check/check-docs-counts-sync.mjs index 4e74d304647..116d33bcc6e 100644 --- a/scripts/check/check-docs-counts-sync.mjs +++ b/scripts/check/check-docs-counts-sync.mjs @@ -185,6 +185,9 @@ export function tallyDrift(checks, getContent) { function readCodeFacts() { const script = [ 'import {computeFreeModelTotals,FREE_MODEL_BUDGETS} from "./open-sse/config/freeModelCatalog.ts";', + 'import {FREE_TIER_PROVIDER_SET} from "./open-sse/config/freeTierProviders.ts";', + 'import {generateProviderPluginManifest} from "./open-sse/config/providerPluginManifestRegistry.ts";', + 'import {REGISTRY} from "./open-sse/config/providers/index.ts";', 'import {MODE_PACKS} from "./open-sse/services/autoCombo/modePacks.ts";', 'import {ENGINE_IDS} from "./open-sse/services/compression/engineCatalog.ts";', 'import {CLI_TOOLS} from "./src/shared/constants/cliTools.ts";', @@ -232,7 +235,10 @@ function readCodeFacts() { "mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size,providers:pids.size,freeForever:ff.size,", "modePacks:Object.keys(MODE_PACKS),", "hardStop:FREE_MODEL_BUDGETS.filter(e=>e.hardStopGuaranteed===true).length,", - "trainsOnPrompts:FREE_MODEL_BUDGETS.filter(e=>e.trainsOnPrompts===true).length}));", + "trainsOnPrompts:FREE_MODEL_BUDGETS.filter(e=>e.trainsOnPrompts===true).length,", + "freeTierCount:FREE_TIER_PROVIDER_SET.size,", + "freeTierReg:FREE_TIER_PROVIDER_SET.size-[...FREE_TIER_PROVIDER_SET].filter(x=>!(x in REGISTRY)).length,", + 'manifestFreeTier:generateProviderPluginManifest().providers.filter(p=>p.capabilities.includes("free-tier")).length}));', ].join(""); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "docs-counts-")); try { @@ -608,6 +614,22 @@ export function buildChecks() { files: ["docs/routing/AUTO-COMBO.md"], validate: makeModePackNamesValidator(packs), }, + { + // Every pack must pin `quality` explicitly (6 pins, 0.02/0.03) so + // no pack silently inherits a future DEFAULT. Validates live code, + // not docs — the gate loops `files` content through `validate`. + label: "mode packs pin quality explicitly (live code)", + actual: 6, + docKey: "packs quality pins", + strict: true, + files: ["open-sse/services/autoCombo/modePacks.ts"], + validate: (content) => { + const pins = (content.match(/^\s*quality:\s*0\.0\d,?\s*$/gm) ?? []).length; + return pins >= 6 + ? { ok: true, detail: `${pins} quality pins` } + : { ok: false, detail: `only ${pins} quality pins — every pack must pin quality` }; + }, + }, { label: "Provider reference total (doc vs live modules)", actual: f.providers, @@ -616,6 +638,23 @@ export function buildChecks() { files: ["docs/reference/PROVIDER_REFERENCE.md"], validate: makeProviderReferenceValidator(f.providers), }, + // Gate: manifest emission vs catalogue intersection. Both numbers are + // live code facts from the same spawnSync computeur. The catalogue can + // name providers the registry does not serve yet (arcee-ai at + // 9d1a896c6), so the leaf raw size is informational — the gate + // compares the intersected count, never the raw size, and the script + // itself always exists so tallyDrift runs validate (skips null only). + { + label: "Manifest free-tier capability count (live code)", + actual: f.manifestFreeTier, + docKey: "free-tier capability", + strict: true, + files: ["scripts/check/check-docs-counts-sync.mjs"], + validate: () => ({ + ok: f.manifestFreeTier === f.freeTierReg, + detail: `manifest ${f.manifestFreeTier} vs leaf∩registry ${f.freeTierReg} (leaf ${f.freeTierCount})`, + }), + }, { label: "SVG canonical numbers (live code)", actual: diff --git a/scripts/check/check-pricing-freshness.mjs b/scripts/check/check-pricing-freshness.mjs new file mode 100644 index 00000000000..075077d13d7 --- /dev/null +++ b/scripts/check/check-pricing-freshness.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node +// STRICT gate: KNOWN_MODEL_PRICING must have been touched within PRICING_STALE_AFTER_DAYS, +// or tiers silently drift from real prices. Exits 1 on stale, 0 on fresh. +// Run: node scripts/check/check-pricing-freshness.mjs +import { execFileSync } from "node:child_process"; + +const PRICING_STALE_AFTER_DAYS = 90; +const TARGET = "open-sse/services/providerCostData.ts"; + +function lastTouchDays() { + const out = execFileSync("git", ["log", "--follow", "-1", "--format=%ct", "--", TARGET], { + encoding: "utf8", + }).trim(); + const touched = Number(out); + if (!Number.isFinite(touched) || touched <= 0) return Infinity; + return (Date.now() / 1000 - touched) / 86400; +} + +const days = lastTouchDays(); +if (days > PRICING_STALE_AFTER_DAYS) { + console.error( + `STALE: ${TARGET} untouched for ${Math.floor(days)}d (> ${PRICING_STALE_AFTER_DAYS}d) — refresh prices or bump the gate with justification` + ); + process.exit(1); +} +console.log(`pricing fresh: ${TARGET} touched ${Math.floor(days)}d ago`); diff --git a/scripts/check/check-provider-order-sync.mjs b/scripts/check/check-provider-order-sync.mjs new file mode 100644 index 00000000000..303ca4d43f1 --- /dev/null +++ b/scripts/check/check-provider-order-sync.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node +// Checks the canonical provider order stays single-sourced. +// +// STRICT (blocking, exit 1): the order derivation exists exactly once +// (canonicalProviderOrder.ts); catalogOrder.ts and comboSort.ts re-export it +// (grep-negative for a local Object.keys(OAUTH derivation); re-export present); +// the xao alias stays declared under xai-oauth; the unknown-provider contract +// (?? Infinity + codeUnitCompare) stays live. +// INFORMATIVE (warn only, always exit 0): dashboard quota ranks in +// ProviderLimits/constants.ts are a separate display order — a registry id +// missing there is fine when the provider has no quota surface. +// Run: node scripts/check/check-provider-order-sync.mjs + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, "..", ".."); +const read = (p) => fs.readFileSync(path.join(ROOT, p), "utf8"); +let failures = 0; +const fail = (msg) => { + failures += 1; + console.error(`[provider-order-sync] FAIL ${msg}`); +}; + +// 1+2. Leaf uniqueness. +for (const f of ["src/app/api/v1/models/catalogOrder.ts", "src/lib/combos/comboSort.ts"]) { + if (read(f).includes("Object.keys(OAUTH")) { + fail(`${f} defines its own order derivation (import the leaf instead)`); + } +} +// 3. xao alias form. +const oauth = read("src/shared/constants/providers/oauth.ts"); +if (!/"xai-oauth":\s*\{[^}]*alias:\s*"xao"/s.test(oauth)) { + fail(`oauth.ts: "xai-oauth" entry lost its alias:"xao"`); +} +// 4. Unknown contract alive. +if (!read("src/shared/constants/canonicalProviderOrder.ts").includes("?? Infinity")) { + fail(`canonicalProviderOrder.ts lost the ?? Infinity unknown contract`); +} +if (!read("src/app/api/v1/models/catalogOrder.ts").includes("codeUnitCompare")) { + fail(`catalogOrder.ts lost the codeUnitCompare unknown-branch`); +} +// 5. Re-export intact. +if (!read("src/lib/combos/comboSort.ts").includes("CANONICAL_PROVIDER_ORDER as PROVIDER_ORDER")) { + fail(`comboSort.ts lost the PROVIDER_ORDER re-export`); +} + +// Informative dashboard check (never fails). +try { + const dash = read("src/app/(dashboard)/dashboard/usage/components/ProviderLimits/constants.ts"); + const ids = new Set(); + // NOTE: apikey/index.ts is a barrel (imports + spreads, zero literal entries), + // so glob the family files instead. + const apikeyFiles = fs + .readdirSync(path.join(ROOT, "src/shared/constants/providers/apikey")) + .filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts")) + .map((f) => `src/shared/constants/providers/apikey/${f}`); + for (const f of [ + "src/shared/constants/providers/oauth.ts", + "src/shared/constants/providers/noauth.ts", + ...apikeyFiles, + ]) { + let src; + try { + src = read(f); + } catch { + continue; + } + for (const m of src.matchAll(/^ "([^"]+)": \{$/gm)) ids.add(m[1]); + } + for (const id of [...ids].sort()) { + if (!dash.includes(`"${id}"`) && !dash.includes(`${id}:`)) { + console.log( + `[provider-order-sync] warn: ${id} has no dashboard quota rank (ok if no quota surface)` + ); + } + } +} catch (e) { + console.log(`[provider-order-sync] warn: dashboard check skipped (${String(e).slice(0, 120)})`); +} + +if (failures > 0) { + process.exit(1); +} +console.log("[provider-order-sync] OK — single source + alias form + unknown contract"); diff --git a/scripts/quality/run-all-gates.mjs b/scripts/quality/run-all-gates.mjs index 409c8c1d292..aa6b68ea90a 100644 --- a/scripts/quality/run-all-gates.mjs +++ b/scripts/quality/run-all-gates.mjs @@ -29,6 +29,8 @@ const GATES = [ // Group B — fast (<5s) { name: "check:provider-consistency", cmd: ["node", "--import", "tsx", "scripts/check/check-provider-consistency.ts"] }, { name: "check:provider-assets", cmd: ["node", "scripts/check/check-provider-assets.mjs"] }, + { name: "check:pricing-freshness", cmd: ["node", "scripts/check/check-pricing-freshness.mjs"] }, + { name: "check:provider-order-sync", cmd: ["node", "scripts/check/check-provider-order-sync.mjs"] }, { name: "check:public-creds", cmd: ["node", "scripts/check/check-public-creds.mjs"] }, { name: "check:error-helper", cmd: ["node", "scripts/check/check-error-helper.mjs"] }, { name: "check:fetch-targets", cmd: ["node", "scripts/check/check-fetch-targets.mjs"] }, diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/QuotaScrapingFields.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/QuotaScrapingFields.tsx index 240eafea50c..9fe727f0450 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/QuotaScrapingFields.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/QuotaScrapingFields.tsx @@ -1,6 +1,7 @@ "use client"; import { Input } from "@/shared/components"; +import { getProviderConnectionFamilyIds } from "@/shared/constants/providers"; import { providerText, type ProviderMessageTranslator } from "../../providerPageHelpers"; import { @@ -54,7 +55,7 @@ export default function QuotaScrapingFields({ ); } - if (provider === "alibaba" || provider === "alibaba-cn") { + if (getProviderConnectionFamilyIds("alibaba").includes(provider)) { return (
= { session: 0, weekly: 1, mcp_monthly: 2 }; @@ -10,7 +11,7 @@ const CODEX_QUOTA_ORDER: Record = { banked_reset_credits: 4, }; const GLM_FAMILY_PROVIDERS = ["glm", "glm-cn", "glmt", "opencode-go"]; -const KIMI_CODING_PROVIDERS = ["kimi-coding", "kimi-coding-apikey"]; +const KIMI_CODING_PROVIDERS: readonly string[] = getProviderConnectionFamilyIds("kimi-coding"); /** * Providers whose quotas already get a deterministic fixed-window order below diff --git a/src/app/api/models/openrouter-catalog/route.ts b/src/app/api/models/openrouter-catalog/route.ts index dcb8656cd1c..058c72939dd 100644 --- a/src/app/api/models/openrouter-catalog/route.ts +++ b/src/app/api/models/openrouter-catalog/route.ts @@ -10,7 +10,7 @@ import { NextRequest, NextResponse } from "next/server"; import { isAuthenticated } from "@/shared/utils/apiAuth"; import { getOpenRouterCatalog, refreshOpenRouterCatalog } from "@/lib/catalog/openrouterCatalog"; import { getSettings } from "@/lib/db/settings"; -import { isFreeModel } from "@/shared/utils/freeModels"; +import { isFreeForProvider } from "@/shared/utils/freeModels"; export async function GET(req: NextRequest) { // Require authentication (dashboard/API key) @@ -29,7 +29,9 @@ export async function GET(req: NextRequest) { hidePaid = settings?.hidePaidModels === true; } catch {} const applyFilter = (data: T[]): T[] => - hidePaid ? data.filter((m) => isFreeModel("or", m as { id: string; pricing?: unknown })) : data; + hidePaid + ? data.filter((m) => isFreeForProvider("openrouter", m as { id: string; pricing?: unknown })) + : data; const forceRefresh = req.nextUrl.searchParams.get("refresh") === "true"; diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index bcf5de3e941..c09a2636b7d 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { + getProviderConnectionFamilyIds, isClaudeCodeCompatibleProvider, isAnthropicCompatibleProvider, isOpenAICompatibleProvider, @@ -2343,7 +2344,7 @@ export async function GET( const data = await response.json(); let pageModels = config.parseResponse(data); - if (provider === "alibaba" || provider === "alibaba-cn") { + if (getProviderConnectionFamilyIds("alibaba").includes(provider)) { const { parseAlibabaModelStudioModelsForConnection } = await import("./discovery/providerModelsConfig.ts"); pageModels = parseAlibabaModelStudioModelsForConnection( @@ -2372,7 +2373,7 @@ export async function GET( ); } - if (provider === "alibaba" || provider === "alibaba-cn") { + if (getProviderConnectionFamilyIds("alibaba").includes(provider)) { const { shouldUseLiveAlibabaFreeModelDiscovery } = await import("@omniroute/open-sse/services/alibabaFreeTier.ts"); const { scheduleAlibabaFreeTierProbeRefresh } = diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 3e60e8bea25..075f7c27825 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -122,7 +122,7 @@ import { } from "./catalogRequest"; import { incrementCcDiscoveryHitCount } from "@/lib/db/ccDiscoveryMetrics"; import { isUnifiedChatSourceModelSelectable } from "./catalogModelPolicy"; -import { isFreeModel } from "@/shared/utils/freeModels"; +import { isFreeForProvider } from "@/shared/utils/freeModels"; import { isModelExposureAllowed } from "@/shared/utils/modelExposureList"; import { isCodexDiscoveryModelExcluded } from "@/shared/services/codexDiscoveryPolicy"; import { buildErrorBody } from "@omniroute/open-sse/utils/error"; @@ -350,11 +350,9 @@ async function buildUnifiedModelsResponseCore( ): boolean => { if (!hidePaid) return false; const provider = aliasToProviderId[providerKey] || providerKey; - // isFree:true is the first door — custom row kept even when its provider is outside FREE_MODEL_BUDGETS. - if (isFreeModel(provider, { id: modelId, pricing: pricing as any, isFree })) return false; - // hidePaid is on and model is non-free → hidden. No need to consult FREE_MODEL_BUDGETS - // separately: paid on a free-capable provider stays hidden, free on a non-budget provider - // already returned above. + // fetched: free only when provider has a documented free tier — catalog hit or guarded heuristics (`:free`/`0`/`isFree:true`); fetched isFree:true not trusted alone + if (isFreeForProvider(provider, { id: modelId, pricing: pricing as any, isFree })) + return false; return true; }; // #11481: opt-in explicit model exposure allow/deny list — same call sites @@ -1659,10 +1657,10 @@ async function buildUnifiedModelsResponseCore( if (model.isHidden === true) continue; if (isModelHiddenBulk(providerId, modelId, canonicalProviderId)) continue; if (isExcludedByProviderConnections(canonicalProviderId, modelId)) continue; - // #6328: apply hidePaidModels to user-defined custom rows too. - // Custom entries do not carry pricing, so shouldHidePaid() decides - // via FREE_MODEL_IDS_BY_PROVIDER — matches synced/PROVIDER_MODELS. + // #6328: apply hidePaidModels to user-defined custom rows too. A local custom + // row flagged isFree:true stays trusted, even outside the free-tier catalog. if ( + (model as { isFree?: unknown }).isFree !== true && shouldHidePaid( canonicalProviderId, modelId, diff --git a/src/app/api/v1/models/catalogOrder.ts b/src/app/api/v1/models/catalogOrder.ts index c0793911e88..7e0bfce672f 100644 --- a/src/app/api/v1/models/catalogOrder.ts +++ b/src/app/api/v1/models/catalogOrder.ts @@ -24,14 +24,7 @@ * effort-variant scoping, and Claude-mirror gating are untouched. Pure; no DB/IO. */ -import { OAUTH_PROVIDERS, NOAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers"; - -/** Canonical provider precedence, keyed by provider id (not alias). Built once. */ -const CANONICAL_PROVIDER_ORDER: readonly string[] = [ - ...Object.keys(OAUTH_PROVIDERS), - ...Object.keys(NOAUTH_PROVIDERS), - ...Object.keys(APIKEY_PROVIDERS), -]; +import { COMBO_GROUP, groupSortPriority } from "@/shared/constants/canonicalProviderOrder"; /** * Locale-independent code-unit comparator. UTF-16 code units put uppercase A-Z @@ -41,9 +34,6 @@ function codeUnitCompare(a: string, b: string): number { return a < b ? -1 : a > b ? 1 : 0; } -/** Combo bucket key, distinct from any real provider id. */ -const COMBO_GROUP = " combo"; - /** * Grouping key for a catalog row: "combo" for combo-owned rows, else owned_by * (canonical identity). Rows with no usable owned_by fall back to the id-prefix; @@ -59,16 +49,6 @@ function modelGroupKey(model: Record): string { return slash > 0 ? id.slice(0, slash) : id; } -/** - * Sort priority for a group key: combo first, then registry precedence, then - * unknown groups (rank Infinity, ordered among themselves by code unit). - */ -function groupSortPriority(groupKey: string): number { - if (groupKey === COMBO_GROUP) return -1; - const idx = CANONICAL_PROVIDER_ORDER.indexOf(groupKey); - return idx >= 0 ? idx : Infinity; -} - /** * Stable provider-grouped sort. Does not mutate the input. Deterministic for a * given input array. diff --git a/src/lib/combos/comboSort.ts b/src/lib/combos/comboSort.ts index 977c45b93c0..316c23b875d 100644 --- a/src/lib/combos/comboSort.ts +++ b/src/lib/combos/comboSort.ts @@ -1,5 +1,5 @@ // src/lib/combos/comboSort.ts -import { OAUTH_PROVIDERS, NOAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers"; +import { CANONICAL_PROVIDER_ORDER, providerRank } from "@/shared/constants/canonicalProviderOrder"; import type { ComboStep } from "@/lib/combos/steps"; export type { ComboStep }; @@ -17,13 +17,8 @@ export function isValidSortMethod(raw: unknown): raw is SortMethod { return VALID_SORT_METHODS.has(raw as string); } -// Mirrors CANONICAL_PROVIDER_ORDER from src/app/api/v1/models/catalogOrder.ts — keep in sync. -// Both are derived from OAUTH+NOAUTH+APIKEY keys; drift would silently diverge catalog vs combo order. -export const PROVIDER_ORDER: readonly string[] = [ - ...Object.keys(OAUTH_PROVIDERS), - ...Object.keys(NOAUTH_PROVIDERS), - ...Object.keys(APIKEY_PROVIDERS), -]; +// Single source: re-exported from canonicalProviderOrder (was a duplicated derivation). +export { CANONICAL_PROVIDER_ORDER as PROVIDER_ORDER }; const REFERENCE_SENTINEL = " combo-ref"; // sorts after any real provider id @@ -40,12 +35,6 @@ function nameKey(step: ComboStep): string { return step.comboName; // combo-ref } -/** Stable index for provider ordering; unknown providers go after the known list. */ -function providerRank(providerId: string): number { - const idx = PROVIDER_ORDER.indexOf(providerId); - return idx === -1 ? PROVIDER_ORDER.length : idx; -} - /** Synchronous sorts: manual (noop), provider, name. Stable. */ export function sortComboStepsSync( steps: ComboStep[], diff --git a/src/lib/db/settings/pricing.ts b/src/lib/db/settings/pricing.ts index b0f7dd1b74b..e2496550a27 100644 --- a/src/lib/db/settings/pricing.ts +++ b/src/lib/db/settings/pricing.ts @@ -13,6 +13,16 @@ type PricingByProvider = Record; export type PricingSource = "default" | "litellm" | "modelsDev" | "user"; export type PricingSourceMap = Record>; +async function touchPricing(): Promise { + invalidateDbCache("pricing"); + try { + const { clearTierCache } = await import("@omniroute/open-sse/services/tierResolver"); + clearTierCache(); + } catch { + // fail-open: a missed tier invalidation must never break a price write + } +} + function readPricingNamespace( db: ReturnType, namespace: string @@ -195,7 +205,7 @@ export async function updatePricing(pricingData: PricingByProvider) { }); tx(); backupDbFile("pre-write"); - invalidateDbCache("pricing"); // Bust the pricing read cache + await touchPricing(); const updated: PricingByProvider = {}; const allRows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing'").all(); for (const row of allRows) { @@ -234,6 +244,7 @@ export async function resetPricing(provider: string, model?: string) { } backupDbFile("pre-write"); + await touchPricing(); const allRows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing'").all(); const result: Record = {}; for (const row of allRows) { @@ -250,5 +261,6 @@ export async function resetAllPricing() { const db = getDbInstance(); db.prepare("DELETE FROM key_value WHERE namespace = 'pricing'").run(); backupDbFile("pre-write"); + await touchPricing(); return {}; } diff --git a/src/shared/constants/canonicalProviderOrder.ts b/src/shared/constants/canonicalProviderOrder.ts new file mode 100644 index 00000000000..9ec86ea4d65 --- /dev/null +++ b/src/shared/constants/canonicalProviderOrder.ts @@ -0,0 +1,48 @@ +import { OAUTH_PROVIDERS } from "@/shared/constants/providers/oauth"; +import { NOAUTH_PROVIDERS } from "@/shared/constants/providers/noauth"; +import { APIKEY_PROVIDERS } from "@/shared/constants/providers/apikey"; + +/** Registry-canonical provider precedence: OAUTH -> NOAUTH -> APIKEY (canonical keys, not aliases). */ +export const CANONICAL_PROVIDER_ORDER: readonly string[] = [ + ...Object.keys(OAUTH_PROVIDERS), + ...Object.keys(NOAUTH_PROVIDERS), + ...Object.keys(APIKEY_PROVIDERS), +]; + +// Single load-time pass (never resolve per sorted item: getProviderByAlias loops +// ~10 sections x providers, which costs more than the indexOf it replaces on ~1k catalog rows). +function buildRankResolved(): ReadonlyMap { + const rank = new Map(); + CANONICAL_PROVIDER_ORDER.forEach((id, i) => rank.set(id, i)); + // Canonical keys win over homonym aliases (m-a: ~17 aliases like minimax-cn + // are also canonical keys — an alias must never steal a canonical slot). + const canonicals = new Set(CANONICAL_PROVIDER_ORDER); + for (const def of Object.values({ + ...OAUTH_PROVIDERS, + ...NOAUTH_PROVIDERS, + ...APIKEY_PROVIDERS, + })) { + const a = (def as { id: string; alias?: string }).alias; + if (typeof a === "string" && a.length > 0 && !canonicals.has(a) && rank.has(def.id)) { + rank.set(a, rank.get(def.id) as number); + } + } + return rank; +} + +const RANK_RESOLVED: ReadonlyMap = buildRankResolved(); + +/** Finite rank (combos): alias resolved, unknown -> length (existing comboSort contract). */ +export function providerRank(providerId: string): number { + return RANK_RESOLVED.get(providerId) ?? CANONICAL_PROVIDER_ORDER.length; +} + +/** Combo bucket key, distinct from any real provider id. Single definition (was also in catalogOrder.ts:45). */ +export const COMBO_GROUP = " combo"; + +/** Catalog priority: combo first, then registry precedence, unknown -> Infinity (code-unit branch stays live). */ +export function groupSortPriority(groupKey: string): number { + if (groupKey === COMBO_GROUP) return -1; + const r = RANK_RESOLVED.get(groupKey); + return r ?? Infinity; +} diff --git a/src/shared/utils/freeModels.ts b/src/shared/utils/freeModels.ts index 5f1f1c4a63c..279e8cc2951 100644 --- a/src/shared/utils/freeModels.ts +++ b/src/shared/utils/freeModels.ts @@ -29,10 +29,10 @@ import { AI_MODELS } from "@/shared/constants/models"; * import modal's preview disagree with the import route that runs on click. * * A provider is considered to "have free models" when it appears in the - * documented free-tier catalog (`FREE_MODEL_BUDGETS`). A single model is - * considered free when its id carries the OpenRouter-style `:free` suffix, when - * both its prompt and completion prices are zero, or when its id is listed as a - * free model for that provider in the catalog. + * documented free-tier catalog (`FREE_MODEL_BUDGETS`). A single model is considered free when — for a provider with a documented + * free tier — its id carries the OpenRouter-style `:free` suffix or both its + * prompt and completion prices are zero (guarded heuristics), or when its id + * is listed as a free model for that provider in the shipped catalog. * * The catalog also records the regime of every entry via `freeType` * (`FreeModelFreeType`). A regime can retire a free tier behind a paid key @@ -84,21 +84,34 @@ export interface FreeModelCandidate { isFree?: boolean; } -/** Whether a single fetched model qualifies as free for the given provider (id or alias). */ -export function isFreeModel(provider: string, model: FreeModelCandidate): boolean { +/** Shipped-catalog entry for this provider (id or alias): trusted on its own. */ +function isCatalogFreeModel(provider: string, modelId: unknown): boolean { + if (typeof modelId !== "string") return false; + return ( + FREE_MODEL_IDS_BY_PROVIDER.get(provider)?.has(modelId) === true || + FREE_MODEL_IDS_BY_PROVIDER.get(resolveProviderId(provider))?.has(modelId) === true + ); +} + +/** Payload-supplied free signals: `isFree:true`, a `:free` id suffix, or zero prices. */ +function hasPayloadFreeSignal(model: FreeModelCandidate): boolean { if (model.isFree === true) return true; if (typeof model.id === "string" && model.id.endsWith(":free")) return true; - if (isZeroPrice(model.pricing?.prompt) && isZeroPrice(model.pricing?.completion)) return true; - if (typeof model.id === "string") { - const canonical = resolveProviderId(provider); - if ( - FREE_MODEL_IDS_BY_PROVIDER.get(provider)?.has(model.id) || - FREE_MODEL_IDS_BY_PROVIDER.get(canonical)?.has(model.id) - ) { - return true; - } - } - return false; + return isZeroPrice(model.pricing?.prompt) && isZeroPrice(model.pricing?.completion); +} + +/** + * Whether a single fetched model qualifies as free for the given provider (id or alias): a + * shipped-catalog entry, or a payload signal on a provider with a documented free tier. + */ +export function isFreeModel(provider: string, model: FreeModelCandidate): boolean { + if (isCatalogFreeModel(provider, model.id)) return true; + return providerHasFreeModels(provider) && hasPayloadFreeSignal(model); +} + +/** Reusable free predicate for fetched payloads — provider must have a documented free tier. */ +export function isFreeForProvider(provider: string, model: FreeModelCandidate): boolean { + return providerHasFreeModels(provider) && isFreeModel(provider, model); } export interface SelectModelsForImportResult { @@ -191,7 +204,7 @@ export function matchesOnlyPaidModels(pattern: string): boolean { const fullId = `${m.provider}/${m.model}`; if (!regex.test(fullId)) continue; matched = true; - if (isFreeModel(m.provider, { id: m.model })) return false; + if (isFreeForProvider(m.provider, { id: m.model })) return false; } return matched; } diff --git a/tests/integration/manifest-routing.test.ts b/tests/integration/manifest-routing.test.ts index 458b9af4ad9..878eda7efb5 100644 --- a/tests/integration/manifest-routing.test.ts +++ b/tests/integration/manifest-routing.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { generateRoutingHints } from "../../open-sse/services/manifestAdapter.ts"; test("manifest routing generates hints without error", async () => { - const hints = generateRoutingHints([], { + const hints = await generateRoutingHints([], { messages: [{ content: "Test" }], }); assert.equal(hints.specificityLevel, "trivial"); @@ -11,7 +11,7 @@ test("manifest routing generates hints without error", async () => { }); test("manifest routing failure gracefully falls back - empty targets handled", async () => { - const hints = generateRoutingHints([], { + const hints = await generateRoutingHints([], { messages: [{ content: "Hello world" }], }); assert.equal(hints.eligibleTargets.length, 0); @@ -20,7 +20,7 @@ test("manifest routing failure gracefully falls back - empty targets handled", a }); test("specificity score is non-negative and bounded", async () => { - const hints = generateRoutingHints([], { + const hints = await generateRoutingHints([], { messages: [{ content: "Hello" }], }); assert.ok(hints.specificity.score >= 0); @@ -28,7 +28,7 @@ test("specificity score is non-negative and bounded", async () => { }); test("routing hints contain all required fields", async () => { - const hints = generateRoutingHints([], { + const hints = await generateRoutingHints([], { messages: [{ content: "Test message" }], }); assert.ok("specificityLevel" in hints); @@ -40,7 +40,7 @@ test("routing hints contain all required fields", async () => { }); test("trivial query recommends free tier", async () => { - const hints = generateRoutingHints([], { + const hints = await generateRoutingHints([], { messages: [{ content: "Hello" }], }); assert.equal(hints.recommendedMinTier, "free"); diff --git a/tests/unit/auto-combo-budget.test.ts b/tests/unit/auto-combo-budget.test.ts new file mode 100644 index 00000000000..1e9313ba58f --- /dev/null +++ b/tests/unit/auto-combo-budget.test.ts @@ -0,0 +1,57 @@ +// tests/unit/auto-combo-budget.test.ts +import test from "node:test"; +import assert from "node:assert/strict"; +import { selectProvider } from "../../open-sse/services/autoCombo/engine.ts"; +import { getSelfHealingManager } from "../../open-sse/services/autoCombo/selfHealing.ts"; +import { DEFAULT_WEIGHTS } from "../../open-sse/services/autoCombo/scoring.ts"; + +const healer = getSelfHealingManager(); +test.beforeEach(() => { + healer.exclusions.clear(); + healer.incidentMode = false; +}); + +function config(over: Record = {}) { + return { + id: "budget-probe", + name: "Budget Probe", + type: "auto", + candidatePool: [], + weights: DEFAULT_WEIGHTS, + explorationRate: 0, + ...over, + } as never; +} + +function cand(provider: string, model: string, cost: number) { + return { + provider, + model, + quotaRemaining: 95, + quotaTotal: 100, + circuitBreakerState: "CLOSED", + costPer1MTokens: cost, + p95LatencyMs: 120, + latencyStdDev: 8, + } as never; +} + +test("default budget math is unchanged without the new field", () => { + const c = [cand("openai", "gpt-4o", 5)]; + const r = selectProvider(config({ budgetCap: 1 }), c); + assert.equal(r.provider, "openai"); // (5/1M)*1000 = $0.005 < $1 +}); + +test("32k estimated tokens enforce a 32x tighter budget", () => { + const c = [cand("openai", "gpt-4o", 5)]; + // (5/1M)*32000 = $0.16 — exceeds a $0.01 cap only when tokens are honored. + // Message verified: "No candidate fits within the configured budget cap…" (engine.ts:59-62). + assert.throws( + () => + selectProvider( + config({ budgetCap: 0.01, budgetFallback: "strict", estimatedInputTokens: 32000 }), + c + ), + /budget cap/i + ); +}); diff --git a/tests/unit/auto-combos-suffixes-4235.test.ts b/tests/unit/auto-combos-suffixes-4235.test.ts index e09e7e9a469..c67ebf383fb 100644 --- a/tests/unit/auto-combos-suffixes-4235.test.ts +++ b/tests/unit/auto-combos-suffixes-4235.test.ts @@ -88,6 +88,16 @@ test("#4235 reliability-first mode pack exists and is normalized", () => { assert.ok(pack.health >= 0.3, "reliability-first leans on circuit-breaker health"); }); +test("every pack carries quality>0 and reliability>0", () => { + for (const [name, w] of Object.entries(modePacks.MODE_PACKS)) { + const weights = w as { quality?: unknown; reliability?: unknown } & Record; + assert.ok(Number(weights.quality) > 0, `${name} quality>0`); + assert.ok(Number(weights.reliability) > 0, `${name} reliability>0`); + const sum = Object.values(weights).reduce((a: number, b: unknown) => a + Number(b), 0); + assert.ok(Math.abs(sum - 0.9999) < 0.002, `${name} sum ~0.9999 got ${sum}`); + } +}); + test("#4235 createBuiltinAutoCombo composes tier weights for auto/coding:fast", async () => { const combo = await builtinCatalog.createBuiltinAutoCombo("auto/coding:fast", "coding:fast"); assert.equal(combo.id, "auto/coding:fast"); diff --git a/tests/unit/canonical-provider-order.test.ts b/tests/unit/canonical-provider-order.test.ts new file mode 100644 index 00000000000..ab5d93fa080 --- /dev/null +++ b/tests/unit/canonical-provider-order.test.ts @@ -0,0 +1,59 @@ +// tests/unit/canonical-provider-order.test.ts +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + CANONICAL_PROVIDER_ORDER, + COMBO_GROUP, + groupSortPriority, + providerRank, +} from "@/shared/constants/canonicalProviderOrder"; +import { OAUTH_PROVIDERS } from "@/shared/constants/providers/oauth"; +import { NOAUTH_PROVIDERS } from "@/shared/constants/providers/noauth"; +import { APIKEY_PROVIDERS } from "@/shared/constants/providers/apikey"; +import { PROVIDER_ORDER } from "@/lib/combos/comboSort"; + +describe("canonical provider order", () => { + it("matches the registry derivation OAUTH -> NOAUTH -> APIKEY", () => { + assert.deepEqual( + [...CANONICAL_PROVIDER_ORDER], + [ + ...Object.keys(OAUTH_PROVIDERS), + ...Object.keys(NOAUTH_PROVIDERS), + ...Object.keys(APIKEY_PROVIDERS), + ] + ); + }); + + it("resolves the xao alias to the xai-oauth rank", () => { + assert.ok(CANONICAL_PROVIDER_ORDER.includes("xai-oauth")); + assert.equal(providerRank("xao"), providerRank("xai-oauth")); + }); + + it("keeps the catalog unknown contract (Infinity, combo first)", () => { + assert.equal(groupSortPriority(COMBO_GROUP), -1); + assert.equal(groupSortPriority("zzz-no-such-provider"), Infinity); + }); + + it("keeps the combos unknown contract (length)", () => { + assert.equal(providerRank("zzz-no-such-provider"), CANONICAL_PROVIDER_ORDER.length); + }); + + it("is shared by reference with comboSort", () => { + assert.equal(PROVIDER_ORDER, CANONICAL_PROVIDER_ORDER); + }); + + it("resolves every declared alias to its canonical rank", () => { + const seen = new Set(); + for (const def of Object.values({ + ...OAUTH_PROVIDERS, + ...NOAUTH_PROVIDERS, + ...APIKEY_PROVIDERS, + }) as { id: string; alias?: string }[]) { + if (typeof def.alias === "string" && def.alias.length > 0 && !seen.has(def.alias)) { + seen.add(def.alias); + assert.equal(providerRank(def.alias), providerRank(def.id)); + } + } + assert.ok(seen.size > 0); + }); +}); diff --git a/tests/unit/check-docs-counts-quality-pinning.test.ts b/tests/unit/check-docs-counts-quality-pinning.test.ts new file mode 100644 index 00000000000..d759f544ad8 --- /dev/null +++ b/tests/unit/check-docs-counts-quality-pinning.test.ts @@ -0,0 +1,22 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { MODE_PACKS } from "../../open-sse/services/autoCombo/modePacks.ts"; + +// Every pack pins quality explicitly so none silently inherits a future DEFAULT. +// Locks, not TDD: green on existing behavior, break on silent regression. + +test("every pack pins quality explicitly (no silent DEFAULT inheritance)", () => { + for (const [name, pack] of Object.entries(MODE_PACKS)) { + assert.ok( + typeof (pack as { quality?: unknown }).quality === "number", + `${name} must pin quality` + ); + } +}); + +test("scoring fallback stays neutral (no DEFAULT inheritance)", () => { + const src = readFileSync("open-sse/services/autoCombo/scoring.ts", "utf8"); + assert.match(src, /weights\.quality \?\? 0\)/); + assert.match(src, /factors\.quality \?\? 0\.5\)/); +}); diff --git a/tests/unit/check-docs-counts-sync.test.ts b/tests/unit/check-docs-counts-sync.test.ts index 7bafffec8c6..a01ff43d4f8 100644 --- a/tests/unit/check-docs-counts-sync.test.ts +++ b/tests/unit/check-docs-counts-sync.test.ts @@ -20,6 +20,7 @@ const tally = tallyDrift as ( docKey: string; strict: boolean; files: string[]; + validate?: (content: string) => { ok: boolean; detail: string }; }[], getContent: (file: string) => string | null ) => { strict: number; soft: number; lines: string[] }; @@ -568,3 +569,60 @@ test("neither claim fires on the other numbers the page is full of", () => { assert.equal(makeValidator(7, HARD_STOP_CLAIM)(page).ok, true); assert.equal(makeValidator(13, TRAINING_CLAIM)(page).ok, true); }); + +function freeTierCheck(freeTierCount: number, manifestFreeTier: number, freeTierReg?: number) { + const reg = freeTierReg ?? freeTierCount; + return { + label: "Manifest free-tier capability count (live code)", + actual: manifestFreeTier, + docKey: "free-tier capability", + strict: true, + files: ["scripts/check/check-docs-counts-sync.mjs"], + validate: () => ({ + ok: manifestFreeTier === reg, + detail: `manifest ${manifestFreeTier} vs leaf∩registry ${reg} (leaf ${freeTierCount})`, + }), + }; +} + +test("gate flags STRICT when manifest free-tier count differs from leaf∩registry", () => { + const r = tally([freeTierCheck(5, 4, 5)], () => "live"); + assert.equal(r.strict, 1); +}); + +test("gate passes when counts agree", () => { + const r = tally([freeTierCheck(5, 5, 5)], () => "live"); + assert.equal(r.strict, 0); +}); + +test("gate passes on the arcee-ai shape: leaf=5, reg=4, manifest=4 (catalogue-only leaf)", () => { + // arcee-ai is in FREE_TIER_PROVIDER_SET (leaf) but absent from REGISTRY, so the + // prod gate compares the manifest against the intersection (reg), not the raw leaf. + const r = tally([freeTierCheck(5, 4, 4)], () => "live"); + assert.equal(r.strict, 0); + // ...while the same manifest against the raw leaf (reg=leaf=5) must stay red. + const stale = tally([freeTierCheck(5, 4, 5)], () => "live"); + assert.equal(stale.strict, 1); +}); + +test("gate skips when readCodeFacts fails (actual 0 fallback)", () => { + const r = tally( + [ + { + label: "Code-derived counts", + actual: 0, + docKey: "code facts", + strict: false, + files: [] as string[], + }, + ], + () => "live" + ); + assert.equal(r.strict, 0); +}); + +test("buildChecks contains a free-tier gate", async () => { + const { buildChecks } = await import("../../scripts/check/check-docs-counts-sync.mjs"); + const checks = (buildChecks as () => { label: string }[])(); + assert.ok(checks.some((c) => c.label.includes("free-tier"))); +}); diff --git a/tests/unit/combo-auto-tier.test.ts b/tests/unit/combo-auto-tier.test.ts new file mode 100644 index 00000000000..67a1bea460c --- /dev/null +++ b/tests/unit/combo-auto-tier.test.ts @@ -0,0 +1,38 @@ +// tests/unit/combo-auto-tier.test.ts — runner node:test +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { calculateTierScore } from "../../open-sse/services/autoCombo/scoring.ts"; + +describe("combo auto tier projection", () => { + it("ultra > pro > standard > free discriminant (order only, no absolutes)", () => { + const ultra = calculateTierScore("ultra", 86400); + const pro = calculateTierScore("pro", 86400); + const std = calculateTierScore("standard", 86400); + const free = calculateTierScore("free", 86400); + // Formula is base*0.8 + resetBonus*0.2 — assert order, never absolute values + assert.ok(ultra > pro && pro > std && std > free); + }); + it("unknown tier falls back to standard base (order preserved)", () => { + const undef = calculateTierScore(undefined, 86400); + const std = calculateTierScore("standard", 86400); + assert.equal(undef, std); + }); + it("resetBonus short interval higher than long (same tier)", () => { + const sShort = calculateTierScore("standard", 3600); + const sLong = calculateTierScore("standard", 86400); + assert.ok(sShort > sLong); + }); + it("projected tier discriminates vs neutral (shared helper, no file read)", async () => { + // Imports the SAME pure helper combo.ts uses — no local + // mirror, so prod/test divergence fails here instead of shipping green. + const { projectAccountTier } = await import("../../open-sse/services/autoCombo/scoring.ts"); + const sUltra = calculateTierScore(projectAccountTier({ accountTier: "ultra" }), 86400); + const sNeutral = calculateTierScore(projectAccountTier({}), 86400); + const sPsd = calculateTierScore( + projectAccountTier({ providerSpecificData: { accountTier: "pro" } }), + 86400 + ); + assert.ok(sUltra > sNeutral, `ultra ${sUltra} should exceed neutral ${sNeutral}`); + assert.ok(sPsd > sNeutral, `psd-pro ${sPsd} should exceed neutral ${sNeutral}`); + }); +}); diff --git a/tests/unit/combo-bootstrap-latency.test.ts b/tests/unit/combo-bootstrap-latency.test.ts new file mode 100644 index 00000000000..0499d7c36a5 --- /dev/null +++ b/tests/unit/combo-bootstrap-latency.test.ts @@ -0,0 +1,53 @@ +// tests/unit/combo-bootstrap-latency.test.ts +import { test } from "node:test"; +import assert from "node:assert/strict"; + +// Median helper lives in combo.ts; import it directly (export it non-default). +import { + poolMedianP95Ms, + bootstrapMs, + shouldWarnBootstrap, + resetBootstrapCounters, + bootstrapLatencyHits, + bootstrapLatencyTotal, +} from "../../open-sse/services/combo.ts"; + +test("median ignores corrupt entries", () => { + const m = poolMedianP95Ms({ + "a/x": { p95LatencyMs: 1000 }, + "b/y": { p95LatencyMs: NaN }, + "c/z": { p95LatencyMs: -5 }, + "d/w": { p95LatencyMs: 3000 }, + } as never); + assert.equal(m, 1000); +}); + +test("empty stats yield undefined (caller falls back to 1500, no cold-start alarm)", () => { + assert.equal(poolMedianP95Ms({}), undefined); +}); + +test("counters start at zero", () => { + resetBootstrapCounters(); + assert.equal(bootstrapLatencyHits, 0); + assert.equal(bootstrapLatencyTotal, 0); +}); + +test("table hit bumps Total but not Hits; miss bumps both", () => { + resetBootstrapCounters(); + assert.equal(bootstrapMs("gpt-4o-mini", 9999), 2764); // table hit + assert.equal(bootstrapLatencyTotal, 1); + assert.equal(bootstrapLatencyHits, 0); + assert.equal(bootstrapMs("some-new-model-xyz", 1234), 1234); // miss -> median + assert.equal(bootstrapLatencyTotal, 2); + assert.equal(bootstrapLatencyHits, 1); + assert.equal(bootstrapMs("another-new-model", undefined), 1500); // miss, no median + resetBootstrapCounters(); +}); + +test("warn gate is pure: cold start and low ratio stay silent, dominant warns once", () => { + assert.equal(shouldWarnBootstrap(10, 10, false, 9999, 0), false); // cold start + assert.equal(shouldWarnBootstrap(2, 10, true, 9999, 0), false); // ratio <= 0.3 + assert.equal(shouldWarnBootstrap(5, 10, true, 9999, 0), false); // throttled 1h + assert.equal(shouldWarnBootstrap(5, 10, true, 3600_001, 0), true); // window elapsed + assert.equal(shouldWarnBootstrap(5, 10, true, 9999, 9000), false); // throttled 1h +}); diff --git a/tests/unit/complexity-router.test.ts b/tests/unit/complexity-router.test.ts index 7a6e6745c75..de7b565d75a 100644 --- a/tests/unit/complexity-router.test.ts +++ b/tests/unit/complexity-router.test.ts @@ -78,8 +78,8 @@ test("classifyRequestComplexity — tool schemas escalate the tier above free", assert.notEqual(c.recommendedTier, "free", "tool-using requests must not route to the free tier"); }); -test("buildComplexityRoutingHint — a tool-using request floors the hint tier above free", () => { - const hint = buildComplexityRoutingHint( +test("buildComplexityRoutingHint — a tool-using request floors the hint tier above free", async () => { + const hint = await buildComplexityRoutingHint( modelTargets(), { messages: [{ role: "user", content: "weather?" }], @@ -96,8 +96,8 @@ test("buildComplexityRoutingHint — a tool-using request floors the hint tier a ); }); -test("buildComplexityRoutingHint — a null body is safe and still builds a tier-neutral hint", () => { - const hint = buildComplexityRoutingHint(modelTargets(), null, NOOP_LOG); +test("buildComplexityRoutingHint — a null body is safe and still builds a tier-neutral hint", async () => { + const hint = await buildComplexityRoutingHint(modelTargets(), null, NOOP_LOG); assert.ok(hint, "a null body must not throw — messages default to [] and a hint is built"); if (!hint) return; assert.ok( diff --git a/tests/unit/free-models-isfree.test.ts b/tests/unit/free-models-isfree.test.ts index 53217ace478..775df3f7784 100644 --- a/tests/unit/free-models-isfree.test.ts +++ b/tests/unit/free-models-isfree.test.ts @@ -3,10 +3,12 @@ import assert from "node:assert/strict"; import { isFreeModel, providerHasFreeModels } from "../../src/shared/utils/freeModels.ts"; describe("isFreeModel isFree opt-in", () => { - it("isFree:true → free even without :free/pricing/catalog", () => { - assert.equal(isFreeModel("any", { id: "x", isFree: true }), true); - assert.equal(isFreeModel("openai", { id: "gpt-4o", isFree: true }), true); - assert.equal(isFreeModel("local", { id: "my-model", isFree: true }), true); + it("isFree:true fetched → free only for a provider with a documented free tier", async () => { + const { FREE_MODEL_BUDGETS } = await import("@omniroute/open-sse/config/freeModelCatalog"); + const freeProvider = FREE_MODEL_BUDGETS[0].provider; + assert.equal(isFreeModel("any", { id: "x", isFree: true }), false); + assert.equal(isFreeModel("local", { id: "my-model", isFree: true }), false); + assert.equal(isFreeModel(freeProvider, { id: "x", isFree: true }), true); }); it("isFree:false/null/undefined/1/'true' → not free (strict ===true)", () => { const junk: unknown[] = [false, null, undefined, 1, "true"]; @@ -22,9 +24,42 @@ describe("isFreeModel isFree opt-in", () => { assert.equal(providerHasFreeModels("local"), false); assert.equal(providerHasFreeModels("openai"), providerHasFreeModels("openai")); }); - it(":free and pricing 0 still work when isFree absent", () => { - assert.equal(isFreeModel("any", { id: "foo:free" }), true); - assert.equal(isFreeModel("any", { id: "foo", pricing: { prompt: 0, completion: 0 } }), true); + it(":free and pricing 0 still work when isFree absent — only for free-tier providers", async () => { + const { FREE_MODEL_BUDGETS } = await import("@omniroute/open-sse/config/freeModelCatalog"); + const freeProvider = FREE_MODEL_BUDGETS[0].provider; + assert.equal(isFreeModel("any", { id: "foo:free" }), false); + assert.equal(isFreeModel("local", { id: "foo", pricing: { prompt: 0, completion: 0 } }), false); + assert.equal(isFreeModel(freeProvider, { id: "foo:free" }), true); + assert.equal( + isFreeModel(freeProvider, { id: "foo", pricing: { prompt: 0, completion: 0 } }), + true + ); assert.equal(isFreeModel("any", { id: "foo", pricing: { prompt: 0, completion: 1 } }), false); }); + it("selectModelsForImport matches isFreeForProvider on every catalogued free model", async () => { + // FREE_MODEL_IDS_BY_PROVIDER is module-private, so rebuild (provider, id) pairs from the + // exported FREE_MODEL_BUDGETS: every budgeted provider has a documented free tier. + const { FREE_MODEL_BUDGETS } = await import("@omniroute/open-sse/config/freeModelCatalog"); + const { selectModelsForImport, isFreeForProvider } = + await import("../../src/shared/utils/freeModels.ts"); + for (const { provider } of FREE_MODEL_BUDGETS) { + const m = { id: `${provider}/probe-lock`, pricing: { prompt: 0, completion: 0 } }; + const expected = isFreeForProvider(provider, m); + const { models } = selectModelsForImport(provider, [m], true); + assert.equal(models.length, expected ? 1 : 0, `${provider}/probe-lock`); + } + }); + it("selectModelsForImport drops zero-price models on providers without documented free tier", async () => { + // "local" has no documented free tier, so its zero-price model is not free. + const { selectModelsForImport, providerHasFreeModels } = + await import("../../src/shared/utils/freeModels.ts"); + assert.equal(providerHasFreeModels("local"), false); + const { models, freeFilterEmpty } = selectModelsForImport( + "local", + [{ id: "local/x", pricing: { prompt: 0, completion: 0 } }], + true + ); + assert.deepEqual(models, []); + assert.equal(freeFilterEmpty, true); + }); }); diff --git a/tests/unit/free-tier-providers.test.ts b/tests/unit/free-tier-providers.test.ts new file mode 100644 index 00000000000..bac7e062254 --- /dev/null +++ b/tests/unit/free-tier-providers.test.ts @@ -0,0 +1,27 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + FREE_TIER_PROVIDER_SET, + hasFreeTierProvider, +} from "../../open-sse/config/freeTierProviders.ts"; + +describe("freeTierProviders leaf", () => { + it("contains a known free provider", () => { + assert.equal(hasFreeTierProvider("api-airforce"), true); + }); + it("contains the documented alias target lookup (af is resolved by caller via entry.alias)", () => { + // The leaf stores canonical ids; alias resolution happens in capabilitiesFor + // via entry.alias. Document the contract: leaf answers ids, caller passes both. + assert.equal(FREE_TIER_PROVIDER_SET.size > 0, true); + }); + it("rejects non-string input", () => { + assert.equal(hasFreeTierProvider(undefined), false); + assert.equal(hasFreeTierProvider(null), false); + }); + it("keeps pollinations free despite discontinued entries", () => { + assert.equal(hasFreeTierProvider("pollinations"), true); + }); + it("excludes a provider with no free models", () => { + assert.equal(hasFreeTierProvider("definitely-not-a-provider-xyz"), false); + }); +}); diff --git a/tests/unit/pricing-fallback.test.ts b/tests/unit/pricing-fallback.test.ts new file mode 100644 index 00000000000..13e2f4fffca --- /dev/null +++ b/tests/unit/pricing-fallback.test.ts @@ -0,0 +1,33 @@ +// tests/unit/pricing-fallback.test.ts +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { getModelPricing, KNOWN_MODEL_PRICING } from "../../open-sse/services/providerCostData.ts"; + +// `ai21/jamba-large-1.7` is a documented free model (FREE_MODEL_BUDGETS, +// one-time-initial) that is NOT in the table (grep jamba -> 0 hit). +// `openai/gpt-4o` is paid. Fixtures are verbatim catalog ids — never a suffixed +// invention (Set.has is exact: a mutilated id is no longer in the catalog). +test("off-table free model is free, never premium-priced", () => { + const p = getModelPricing("ai21", "jamba-large-1.7"); + assert.equal(p.isFree, true); + assert.equal(p.inputCostPer1M, 0); + assert.equal(p.outputCostPer1M, 0); +}); + +test("prefixed model id still resolves through the catalog", () => { + const p = getModelPricing("ai21", "ai21/jamba-large-1.7"); + assert.equal(p.isFree, true); +}); + +test("off-table paid model keeps the conservative fallback", () => { + const p = getModelPricing("openai", "gpt-9-never-existed"); + assert.equal(p.isFree, false); + assert.equal(p.inputCostPer1M, 5.0); + assert.equal(p.outputCostPer1M, 15.0); +}); + +test("table hits are untouched", () => { + assert.equal(Object.keys(KNOWN_MODEL_PRICING).length, 21); + assert.deepEqual(getModelPricing("openai", "gpt-4o"), KNOWN_MODEL_PRICING["gpt-4o"]); + assert.deepEqual(getModelPricing("longcat", "LongCat-2.0"), KNOWN_MODEL_PRICING["longcat-2.0"]); +}); diff --git a/tests/unit/provider-plugin-manifest.test.ts b/tests/unit/provider-plugin-manifest.test.ts index c1db75bc82b..85ebba8d1e3 100644 --- a/tests/unit/provider-plugin-manifest.test.ts +++ b/tests/unit/provider-plugin-manifest.test.ts @@ -8,6 +8,7 @@ import { import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; import { USAGE_FETCHER_PROVIDERS } from "../../open-sse/services/usage/fetcherProviders.ts"; import { USAGE_SUPPORTED_PROVIDERS } from "../../open-sse/services/usage/supportedProviders.ts"; +import { hasFreeTierProvider } from "../../open-sse/config/freeTierProviders.ts"; const registryFixture: Record = { openai: { @@ -261,3 +262,138 @@ test("usage-supported matches only on id, not alias (#10078)", () => { ); assert.ok(entry.capabilities.includes("usage-fetch")); }); + +test("manifest emits free-tier for a provider with documented free models", () => { + const entry = getProviderPluginManifestEntryFromRegistry( + { + ...registryFixture, + "api-airforce": { + id: "api-airforce", + alias: "af", + format: "openai", + executor: "default", + baseUrl: "https://api.airforce/v1/chat/completions", + authType: "apikey", + authHeader: "bearer", + defaultContextLength: 128000, + models: [{ id: "x-ai/grok-3", name: "Grok-3 (Free)" }], + }, + "legacy-free": { + id: "legacy-free", + alias: "api-airforce", + format: "openai", + executor: "default", + baseUrl: "https://legacy.example/v1", + authType: "apikey", + authHeader: "bearer", + models: [{ id: "legacy-model", name: "Legacy" }], + }, + "definitely-not-a-provider-xyz": { + id: "definitely-not-a-provider-xyz", + format: "openai", + executor: "default", + baseUrl: "https://xyz.example/v1", + authType: "apikey", + authHeader: "bearer", + models: [{ id: "xyz-model", name: "Xyz" }], + }, + pollinations: { + id: "pollinations", + format: "openai", + executor: "default", + baseUrl: "https://text.pollinations.ai/v1", + authType: "optional", + authHeader: "bearer", + models: [{ id: "openai", name: "OpenAI (free)" }], + }, + }, + "api-airforce" + ); + assert.ok(entry); + assert.ok(entry.capabilities.includes("free-tier")); +}); + +test("manifest emits free-tier via alias-only branch (id outside set, alias inside)", () => { + assert.equal(hasFreeTierProvider("legacy-free"), false); + assert.equal(hasFreeTierProvider("api-airforce"), true); + const entry = getProviderPluginManifestEntryFromRegistry( + { + ...registryFixture, + "legacy-free": { + id: "legacy-free", + alias: "api-airforce", + format: "openai", + executor: "default", + baseUrl: "https://legacy.example/v1", + authType: "apikey", + authHeader: "bearer", + models: [{ id: "legacy-model", name: "Legacy" }], + }, + }, + "legacy-free" + ); + assert.ok(entry); + assert.ok(entry.capabilities.includes("free-tier")); +}); + +test("manifest omits free-tier for a provider with no free models", () => { + const entry = getProviderPluginManifestEntryFromRegistry( + { + ...registryFixture, + "definitely-not-a-provider-xyz": { + id: "definitely-not-a-provider-xyz", + format: "openai", + executor: "default", + baseUrl: "https://xyz.example/v1", + authType: "apikey", + authHeader: "bearer", + models: [{ id: "xyz-model", name: "Xyz" }], + }, + }, + "definitely-not-a-provider-xyz" + ); + assert.ok(entry); + assert.equal(entry.capabilities.includes("free-tier"), false); +}); + +test("manifest emits free-tier for pollinations despite discontinued rows", () => { + const entry = getProviderPluginManifestEntryFromRegistry( + { + ...registryFixture, + pollinations: { + id: "pollinations", + format: "openai", + executor: "default", + baseUrl: "https://text.pollinations.ai/v1", + authType: "optional", + authHeader: "bearer", + models: [{ id: "openai", name: "OpenAI (free)" }], + }, + }, + "pollinations" + ); + assert.ok(entry); + assert.ok(entry.capabilities.includes("free-tier")); +}); + +test("free-tier capability keeps capabilities sorted", () => { + const entry = getProviderPluginManifestEntryFromRegistry( + { + ...registryFixture, + "api-airforce": { + id: "api-airforce", + alias: "af", + format: "openai", + executor: "default", + baseUrl: "https://api.airforce/v1/chat/completions", + authType: "apikey", + authHeader: "bearer", + defaultContextLength: 128000, + models: [{ id: "x-ai/grok-3", name: "Grok-3 (Free)" }], + }, + }, + "api-airforce" + ); + assert.ok(entry); + assert.deepEqual(entry.capabilities, [...entry.capabilities].sort()); +}); diff --git a/tests/unit/tier-pricing-cache.test.ts b/tests/unit/tier-pricing-cache.test.ts new file mode 100644 index 00000000000..844cfaafa63 --- /dev/null +++ b/tests/unit/tier-pricing-cache.test.ts @@ -0,0 +1,50 @@ +// tests/unit/tier-pricing-cache.test.ts +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + classifyTier, + classifyTierAsync, + clearTierCache, +} from "../../open-sse/services/tierResolver.ts"; +import { updatePricing, resetPricing, resetAllPricing } from "../../src/lib/db/settings/pricing.ts"; + +test.beforeEach(() => clearTierCache()); + +test("async tier reflects a DB price write without restart", async () => { + try { + await updatePricing({ testprov: { "testmodel-cache-1": { input: 0, output: 0 } } }); + const a = await classifyTierAsync("testprov", "testmodel-cache-1"); + assert.equal(a.tier, "free"); + } finally { + await resetPricing("testprov", "testmodel-cache-1"); + } +}); + +test("resetAllPricing (full wipe, no read-cache bust) also falls back cleanly", async () => { + try { + await updatePricing({ testprov: { "testmodel-cache-2": { input: 0, output: 0 } } }); + assert.equal((await classifyTierAsync("testprov", "testmodel-cache-2")).tier, "free"); + await resetAllPricing(); + const back = await classifyTierAsync("testprov", "testmodel-cache-2"); + assert.equal(back.tier, classifyTier("testprov", "testmodel-cache-2").tier); + } finally { + await resetPricing("testprov"); + } +}); + +test("async tier falls back to sync when DB is down", async () => { + const a = await classifyTierAsync("openai", "gpt-4o"); + assert.deepEqual(a, classifyTier("openai", "gpt-4o")); +}); + +test("sync tier keeps serving the table while DB changes", async () => { + // Fictitious model on a real provider: never pollutes shared state if cleanup fails. + const before = classifyTier("openai", "gpt-9-never-existed"); + try { + await updatePricing({ openai: { "gpt-9-never-existed": { input: 0, output: 0 } } }); + const during = classifyTier("openai", "gpt-9-never-existed"); + assert.equal(during.tier, before.tier); // sync path untouched by DB + } finally { + await resetPricing("openai", "gpt-9-never-existed"); + } +}); diff --git a/tests/unit/virtualFactory-snapshot-breaker.test.ts b/tests/unit/virtualFactory-snapshot-breaker.test.ts new file mode 100644 index 00000000000..fb046abe959 --- /dev/null +++ b/tests/unit/virtualFactory-snapshot-breaker.test.ts @@ -0,0 +1,90 @@ +// tests/unit/virtualFactory-snapshot-breaker.test.ts +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + computeSnapshotWeights, + type VirtualAutoComboCandidate, +} from "../../open-sse/services/autoCombo/virtualFactory.ts"; +import type { ScoringWeights } from "../../open-sse/services/autoCombo/scoring.ts"; + +describe("computeSnapshotWeights breaker and quality", () => { + const weights: ScoringWeights = { + quota: 0.1, + health: 0.3, + costInv: 0.05, + latencyInv: 0.05, + taskFit: 0.05, + stability: 0.05, + tierPriority: 0.05, + tierAffinity: 0, + specificityMatch: 0, + contextAffinity: 0, + sessionAvailability: 0.05, + resetWindowAffinity: 0, + connectionDensity: 0.05, + quality: 0.1, + reliability: 0, + } as ScoringWeights; + type BreakerMap = ReadonlyMap; + it("OPEN scores lower than CLOSED (same quality)", () => { + const c: VirtualAutoComboCandidate = { + provider: "openai", + model: "gpt-4o", + modelStr: "openai/gpt-4o", + connectionId: null, + costPer1MTokens: 5, + quality: 0.8, + }; + const open: BreakerMap = new Map([["openai", "OPEN"]]); + const closed: BreakerMap = new Map([["openai", "CLOSED"]]); + const sOpen = computeSnapshotWeights([c], weights, open).get("openai/gpt-4o")!; + const sClosed = computeSnapshotWeights([c], weights, closed).get("openai/gpt-4o")!; + assert.ok(sClosed > sOpen, `closed ${sClosed} should > open ${sOpen}`); + }); + it("HALF_OPEN is midpoint, absent breaker → neutral 0.5", () => { + const c: VirtualAutoComboCandidate = { + provider: "openai", + model: "gpt-4o", + modelStr: "openai/gpt-4o", + connectionId: null, + costPer1MTokens: 5, + }; + const half: BreakerMap = new Map([["openai", "HALF_OPEN"]]); + const sHalf = computeSnapshotWeights([c], weights, half).get("openai/gpt-4o")!; + const sNone = computeSnapshotWeights([c], weights).get("openai/gpt-4o")!; + const sOpen = computeSnapshotWeights( + [c], + weights, + new Map([["openai", "OPEN"]]) as BreakerMap + ).get("openai/gpt-4o")!; + const sClosed = computeSnapshotWeights( + [c], + weights, + new Map([["openai", "CLOSED"]]) as BreakerMap + ).get("openai/gpt-4o")!; + assert.ok(sHalf > sOpen && sHalf < sClosed); + // absent → existing 0.5* health — between open and closed + assert.ok(sNone > sOpen && sNone < sClosed); + }); + it("quality cold 0.5 vs hot 0.9 discriminant when weights.quality>0", () => { + const cold: VirtualAutoComboCandidate = { + provider: "openai", + model: "gpt-4o", + modelStr: "openai/gpt-4o", + connectionId: null, + costPer1MTokens: 5, + quality: undefined, + }; + const hot: VirtualAutoComboCandidate = { + provider: "openai", + model: "gpt-4o", + modelStr: "openai/gpt-4o", + connectionId: null, + costPer1MTokens: 5, + quality: 0.9, + }; + const sCold = computeSnapshotWeights([cold], weights).get("openai/gpt-4o")!; + const sHot = computeSnapshotWeights([hot], weights).get("openai/gpt-4o")!; + assert.ok(sHot > sCold); + }); +}); diff --git a/tests/unit/virtualFactory-snapshot-quality.test.ts b/tests/unit/virtualFactory-snapshot-quality.test.ts new file mode 100644 index 00000000000..7aee2c6a857 --- /dev/null +++ b/tests/unit/virtualFactory-snapshot-quality.test.ts @@ -0,0 +1,75 @@ +// tests/unit/virtualFactory-snapshot-quality.test.ts +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + computeSnapshotWeights, + type VirtualAutoComboCandidate, +} from "../../open-sse/services/autoCombo/virtualFactory.ts"; +import type { ScoringWeights } from "../../open-sse/services/autoCombo/scoring.ts"; +import { createModelCapabilityResolutionSnapshot } from "../../src/lib/modelCapabilities.ts"; + +describe("virtualFactory snapshot quality", () => { + it("factory-built candidate carries finite quality (attach path, both pools)", async () => { + // Exercises the REAL hydration path: push a raw candidate through + // attachPreparedCapabilityValues:547-598 (covers regular + family pools — + // both go through attach, so one call proves both). + const { attachPreparedCapabilityValues } = + await import("../../open-sse/services/autoCombo/virtualFactory.ts"); + const raw: VirtualAutoComboCandidate[] = [ + { + provider: "openai", + model: "gpt-4o", + modelStr: "openai/gpt-4o", + connectionId: null, + costPer1MTokens: 5, + }, + ]; + const state = { + byTarget: new Map(), + resolvedSinceYield: 0, + resolutionSnapshot: createModelCapabilityResolutionSnapshot(), + }; + const out = await attachPreparedCapabilityValues(raw, state); + const q = out[0]?.quality; + assert.ok( + typeof q === "number" && Number.isFinite(q) && q >= 0 && q <= 1, + `quality ${q} must be finite 0..1` + ); + }); + it("hot quality outranks cold through computeSnapshotWeights", () => { + const weights = { + quota: 0.1, + health: 0.1, + quality: 0.3, + taskFit: 0, + stability: 0, + tierPriority: 0, + costInv: 0, + latencyInv: 0, + } as ScoringWeights; + const cold: VirtualAutoComboCandidate = { + provider: "openai", + model: "gpt-4o", + modelStr: "openai/gpt-4o", + connectionId: null, + costPer1MTokens: 5, + quality: 0.5, + }; + const hot: VirtualAutoComboCandidate = { + provider: "openai", + model: "gpt-4o", + modelStr: "openai/gpt-4o", + connectionId: null, + costPer1MTokens: 5, + quality: 0.9, + }; + const sCold = computeSnapshotWeights([cold], weights).get("openai/gpt-4o")!; + const sHot = computeSnapshotWeights([hot], weights).get("openai/gpt-4o")!; + assert.ok(sHot > sCold, `hot ${sHot} should exceed cold ${sCold}`); + }); + it("hydration stores a finite 0..1 quality (not NaN)", async () => { + const { getQualityScore } = await import("../../open-sse/services/routing/quality.ts"); + const q = getQualityScore("openai", "gpt-4o"); + assert.ok(typeof q === "number" && Number.isFinite(q) && q >= 0 && q <= 1); + }); +}); diff --git a/tests/unit/virtualFactory-valuation.test.ts b/tests/unit/virtualFactory-valuation.test.ts new file mode 100644 index 00000000000..79f9665c1f5 --- /dev/null +++ b/tests/unit/virtualFactory-valuation.test.ts @@ -0,0 +1,47 @@ +// tests/unit/virtualFactory-valuation.test.ts (part 1 — reliability) +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { computeSnapshotWeights } from "../../open-sse/services/autoCombo/virtualFactory.ts"; +import { resolveVirtualCost } from "../../open-sse/services/providerCostData.ts"; +import { DEFAULT_WEIGHTS } from "../../open-sse/services/autoCombo/scoring.ts"; + +function candidate(over: Record = {}) { + return { + provider: "openai", + connectionId: null, + model: "gpt-4o", + modelStr: "openai/gpt-4o", + costPer1MTokens: 5, + ...over, + } as never; +} + +test("absent failure rate reads fully reliable (neutral 1)", () => { + const weights = { ...DEFAULT_WEIGHTS, reliability: 0.2 }; + const scores = computeSnapshotWeights([candidate()], weights); + const base = computeSnapshotWeights([candidate()], { ...DEFAULT_WEIGHTS, reliability: 0 }); + const delta0 = scores.get("openai/gpt-4o")! - base.get("openai/gpt-4o")!; + assert.ok(Math.abs(delta0 - 0.2) < 1e-9, `expected 0.2, got ${delta0}`); +}); + +test("failureRate 0.2 scales the reliability weight to 0.8", () => { + const weights = { ...DEFAULT_WEIGHTS, reliability: 0.2 }; + const scores = computeSnapshotWeights([candidate({ failureRate: 0.2 })], weights); + const base = computeSnapshotWeights([candidate()], { ...DEFAULT_WEIGHTS, reliability: 0 }); + const delta = scores.get("openai/gpt-4o")! - base.get("openai/gpt-4o")!; + assert.ok(Math.abs(delta - 0.16) < 1e-9, `expected 0.16, got ${delta}`); +}); + +test("errorRate backs up a missing failureRate", () => { + const weights = { ...DEFAULT_WEIGHTS, reliability: 0.2 }; + const scores = computeSnapshotWeights([candidate({ errorRate: 0.5 })], weights); + const base = computeSnapshotWeights([candidate()], { ...DEFAULT_WEIGHTS, reliability: 0 }); + const delta = scores.get("openai/gpt-4o")! - base.get("openai/gpt-4o")!; + assert.ok(Math.abs(delta - 0.1) < 1e-9, `expected 0.1, got ${delta}`); +}); + +test("virtual cost helper returns real table costs, not zero", () => { + assert.equal(resolveVirtualCost("openai", "gpt-4o"), 2.5); + assert.equal(resolveVirtualCost("ai21", "jamba-large-1.7"), 0); // catalog free + assert.equal(resolveVirtualCost("openai", "gpt-9-never-existed"), 5.0); // conservative fallback +});