From 5ca747f6a58b627b1241a28b2bf41ce7979e520e Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 17 Aug 2026 05:48:48 -0300 Subject: [PATCH 01/22] fix(sse): exclude search providers from credential-health scheduler sweep (#10435) * fix(sse): exclude search providers from credential-health scheduler sweep The credential-health scheduler's sweep() tested every active connection every 5 minutes with no exclusion for search providers. For providers in SEARCH_VALIDATOR_CONFIGS (tavily-search, exa-search, serper-search, brave-search, google-pse-search, linkup-search, searchapi-search, youcom-search), "validation" fires a real billed upstream query (e.g. POST api.tavily.com/search), so the periodic sweep silently burned quota with no user-initiated search. Exclude connections whose provider id is registered in SEARCH_VALIDATOR_CONFIGS from the sweep's connection-selection filter. Non-search API-key/OAuth connections remain monitored (#9180, #9289 regressions verified green). Closes #9970 * fix(docs): drop backticks around SEARCH_VALIDATOR_CONFIGS in ENVIRONMENT.md The env/docs sync gate (check-env-doc-sync.mjs) treats any backtick-wrapped SHOUTY_NAME as an env var reference. SEARCH_VALIDATOR_CONFIGS is a code export, not an env var, so wrapping it in backticks made the #9970 doc note trip the env/docs contract check (docMissingEnv). Drop the backticks so the gate stops classifying it as an undocumented env var. --------- Co-authored-by: adevwithpurpose --- ...ential-health-search-provider-exclusion.md | 1 + docs/reference/ENVIRONMENT.md | 2 +- src/lib/credentialHealth/scheduler.ts | 9 +- ...credential-health-search-providers.test.ts | 96 +++++++++++++++++++ 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/9970-credential-health-search-provider-exclusion.md create mode 100644 tests/unit/credential-health-search-providers.test.ts diff --git a/changelog.d/fixes/9970-credential-health-search-provider-exclusion.md b/changelog.d/fixes/9970-credential-health-search-provider-exclusion.md new file mode 100644 index 0000000000..04aef65204 --- /dev/null +++ b/changelog.d/fixes/9970-credential-health-search-provider-exclusion.md @@ -0,0 +1 @@ +- fix(sse): exclude search providers from credential-health scheduler sweep to stop burning billed API queries (#9970) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 3cfb6e4700..6923092247 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -151,7 +151,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari | `OMNIROUTE_SKIP_DB_HEALTHCHECK` | _(unset)_ | `src/lib/db/core.ts` / `src/lib/db/healthCheck.ts` | Set to `1` to skip the SQLite integrity health check on startup. Useful for faster boot on large databases. | | `CREDENTIAL_HEALTH_CHECK_INTERVAL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/scheduler.ts` | Interval (ms) for the background credential health check scheduler. Minimum: 10000 (10s). | | `CREDENTIAL_HEALTH_CACHE_TTL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/cache.ts` | TTL (ms) for cached credential health status. | -| `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` | `false` | `src/lib/credentialHealth/scheduler.ts` | Set to `1` or `true` to disable background periodic testing of provider connections. | +| `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` | `false` | `src/lib/credentialHealth/scheduler.ts` | Set to `1` or `true` to disable background periodic testing of provider connections. Search providers (SEARCH_VALIDATOR_CONFIGS in `src/lib/providers/validation/searchProviders.ts`, e.g. `tavily-search`) are always excluded from the sweep — their "validation" is a real billed upstream query, so they are never health-checked on a timer (#9970). | | `HOST` | `0.0.0.0` | `scripts/dev/run-next.mjs` | Bind address for the Next.js dev/start server. Overrides the default `0.0.0.0` when set. | | `HOSTNAME` | `127.0.0.1` | `scripts/dev/run-next-playwright.mjs` | Bind address used by the Playwright runner when launching Next.js. Defaults to `127.0.0.1` for hermetic tests. **Do not use for `omniroute serve`** — use `OMNIROUTE_SERVER_HOST` instead (POSIX shells auto-set `HOSTNAME` to the machine name; `.env` cannot override it). | | `OMNIROUTE_SERVER_HOST` | `0.0.0.0` | `bin/cli/commands/serve.mjs` | Bind address for `omniroute serve`. Avoids collision with the POSIX shell `HOSTNAME` variable (always set to the machine name by bash/zsh). Falls back to `0.0.0.0` when unset. (#6194) | diff --git a/src/lib/credentialHealth/scheduler.ts b/src/lib/credentialHealth/scheduler.ts index fa4d78614d..6207a26549 100644 --- a/src/lib/credentialHealth/scheduler.ts +++ b/src/lib/credentialHealth/scheduler.ts @@ -25,6 +25,7 @@ import { } from "@/lib/credentialHealth/cache"; import { emit } from "@/lib/events/eventBus"; import { isAutomatedTestProcess } from "@/shared/utils/testProcess"; +import { SEARCH_VALIDATOR_CONFIGS } from "@/lib/providers/validation/searchProviders"; // ── Config ──────────────────────────────────────────────────────────────── @@ -230,7 +231,13 @@ export async function sweep(): Promise { try { const raw = await getProviderConnections({ isActive: true }); connections = (Array.isArray(raw) ? raw : []).filter( - (conn: any) => conn && conn.id && (conn.authType === "apikey" || conn.authType === "oauth") + (conn: any) => + conn && + conn.id && + (conn.authType === "apikey" || conn.authType === "oauth") && + // #9970: search-provider "validation" fires a REAL billed upstream + // query (e.g. POST api.tavily.com/search) — never sweep these. + !(conn.provider in SEARCH_VALIDATOR_CONFIGS) ) as Array<{ id: string; provider: string; diff --git a/tests/unit/credential-health-search-providers.test.ts b/tests/unit/credential-health-search-providers.test.ts new file mode 100644 index 0000000000..2d73da1bdb --- /dev/null +++ b/tests/unit/credential-health-search-providers.test.ts @@ -0,0 +1,96 @@ +/** + * Regression test for #9970 — credential-health scheduler burns real billed + * API queries for search providers. + * + * Search-provider "validation" (SEARCH_VALIDATOR_CONFIGS, e.g. tavily-search) + * issues a real upstream query (POST api.tavily.com/search) rather than a + * cheap auth probe. The scheduler's periodic sweep() must exclude connections + * whose provider id is registered in SEARCH_VALIDATOR_CONFIGS so it never + * fires a billed query on a timer. + * + * Mirrors the source-inspection style of + * tests/unit/credential-health-active-connections-9180.test.ts. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; + +const schedulerSource = fs.readFileSync( + new URL("../../src/lib/credentialHealth/scheduler.ts", import.meta.url), + "utf8" +); + +const searchProvidersSource = fs.readFileSync( + new URL("../../src/lib/providers/validation/searchProviders.ts", import.meta.url), + "utf8" +); + +function getSweepConnectionSelection(): string { + const start = schedulerSource.indexOf("export async function sweep(): Promise"); + assert.notEqual(start, -1, "credential-health sweep must exist"); + + const end = schedulerSource.indexOf("\n if (connections.length === 0) return;", start); + assert.notEqual(end, -1, "credential-health connection-selection block must exist"); + + return schedulerSource.slice(start, end); +} + +test("#9970 scheduler imports SEARCH_VALIDATOR_CONFIGS to classify billed-query providers", () => { + assert.match( + schedulerSource, + /import\s*\{\s*SEARCH_VALIDATOR_CONFIGS\s*\}\s*from\s*"@\/lib\/providers\/validation\/searchProviders"/, + "scheduler.ts must import SEARCH_VALIDATOR_CONFIGS from the search-provider validators module" + ); +}); + +test("#9970 sweep() excludes search providers from the connection-selection filter", () => { + const selection = getSweepConnectionSelection(); + + assert.match( + selection, + /SEARCH_VALIDATOR_CONFIGS/, + "sweep()'s connection-selection block must reference SEARCH_VALIDATOR_CONFIGS to exclude search providers" + ); + + assert.match( + selection, + /!\(conn\.provider in SEARCH_VALIDATOR_CONFIGS\)/, + "sweep() must filter out connections whose provider id is a registered search-validator provider" + ); +}); + +test("#9970 sweep() still keeps API-key + OAuth eligibility intact (no regression on #9180)", () => { + const selection = getSweepConnectionSelection(); + + assert.match( + selection, + /getProviderConnections\(\{\s*isActive:\s*true\s*\}\)/, + "the scheduler must still request only active provider connections" + ); + + assert.match( + selection, + /conn\.authType === "apikey"/, + "API-key connections must remain eligible" + ); + + assert.match(selection, /conn\.authType === "oauth"/, "OAuth connections must remain eligible"); +}); + +test("#9970 trust anchor: SEARCH_VALIDATOR_CONFIGS providers target real billed upstream endpoints", () => { + // Sanity-check the assumption driving the fix: the search validators really + // do fire live upstream queries (not just an auth ping), so excluding them + // from the periodic sweep is the correct trade-off. + assert.match( + searchProvidersSource, + /api\.tavily\.com\/search/, + "tavily-search validator must target the real Tavily search endpoint" + ); + + assert.match( + searchProvidersSource, + /export const SEARCH_VALIDATOR_CONFIGS/, + "SEARCH_VALIDATOR_CONFIGS must be exported so the scheduler can reference it" + ); +}); From b17dfa4a141779d60379a2ad34517ec72bf75a46 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 17 Aug 2026 05:49:16 -0300 Subject: [PATCH 02/22] fix(sse): mark gemini-3.5-flash as thinking-capable (#10450) The base gemini-3.5-flash entry spread the shared GEMINI_35_FLASH_MODEL_SPEC constant, which has supportsThinking:false because it is also spread into several Antigravity flash-tier aliases that reject client-supplied thinking params. That made the reasoning-routing policy resolve reasoning_effort as "unsupported" for the base Google AI Studio model, producing a spurious pre-provider HTTP 400 even though the model supports reasoning (it has an effort-tier alias gemini-3.5-flash-high). Set supportsThinking:true as an explicit override on the base gemini-3.5-flash entry only, leaving the shared spec and the Antigravity tier aliases unchanged. Closes #10286 Co-authored-by: adevwithpurpose --- .../fixes/10286-gemini-3-5-flash-thinking.md | 1 + src/shared/constants/modelSpecs.ts | 8 ++ tests/unit/gemini-3-5-flash-thinking.test.ts | 76 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 changelog.d/fixes/10286-gemini-3-5-flash-thinking.md create mode 100644 tests/unit/gemini-3-5-flash-thinking.test.ts diff --git a/changelog.d/fixes/10286-gemini-3-5-flash-thinking.md b/changelog.d/fixes/10286-gemini-3-5-flash-thinking.md new file mode 100644 index 0000000000..30a3c44bcb --- /dev/null +++ b/changelog.d/fixes/10286-gemini-3-5-flash-thinking.md @@ -0,0 +1 @@ +- fix(sse): mark gemini-3.5-flash as thinking-capable so reasoning_effort is no longer rejected with a spurious 400 (#10286) diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index aef1437926..96f5105a79 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -229,8 +229,16 @@ export const MODEL_SPECS: Record = { }, // ── Gemini 3.5 Flash ───────────────────────────────────────────── + // #10286: the base Google AI Studio model DOES support reasoning (it has + // an effort-tier alias gemini-3.5-flash-high) — override the shared spec's + // supportsThinking:false here only. Do NOT flip GEMINI_35_FLASH_MODEL_SPEC + // itself: it is also spread into the Antigravity flash-tier aliases + // (gemini-3.5-flash-low/-extra-low, gemini-3-flash-agent, gemini-3.6-flash-*) + // which reject client-supplied thinking params because the model id itself + // selects the reasoning tier upstream. "gemini-3.5-flash": { ...GEMINI_35_FLASH_MODEL_SPEC, + supportsThinking: true, aliases: ["gemini-3.5-flash-high"], }, diff --git a/tests/unit/gemini-3-5-flash-thinking.test.ts b/tests/unit/gemini-3-5-flash-thinking.test.ts new file mode 100644 index 0000000000..80636a0b03 --- /dev/null +++ b/tests/unit/gemini-3-5-flash-thinking.test.ts @@ -0,0 +1,76 @@ +// Regression test for #10286: gemini-3.5-flash was incorrectly marked +// supportsThinking:false, causing a spurious pre-provider HTTP 400 for any +// request with reasoning_effort set, even though the base Google AI Studio +// model supports reasoning (it has an effort-tier alias gemini-3.5-flash-high). +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-repro-10286-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "test-repro-10286-secret"; + +const caps = await import("../../src/lib/modelCapabilities.ts"); +const core = await import("../../src/lib/db/core.ts"); +const rulesDb = await import("../../src/lib/db/reasoningRoutingRules.ts"); +const policy = await import("../../src/lib/reasoningRouting/policy.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + rulesDb.invalidateReasoningRoutingRuleCache(); +} + +function ruleInput(patch: Record = {}) { + return { + name: "Enable thinking on gemini-3.5-flash", + description: "", + scope: "global", + apiKeyId: null, + comboId: null, + connectionId: null, + modelPattern: "gemini-3.5-flash", + sourceEffort: "any", + requestTags: [], + tagMatchMode: "any", + effortMode: "inherit", + targetEffort: null, + targetKind: "keep", + targetModel: null, + targetComboId: null, + budgetAction: "preserve", + budgetTokens: null, + priority: 0, + enabled: true, + ...patch, + }; +} + +test.beforeEach(resetStorage); +test.after(async () => { + await resetStorage(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("gemini-3.5-flash (AI Studio provider) resolves as thinking-capable", () => { + const resolved = caps.getResolvedModelCapabilities({ + provider: "gemini", + model: "gemini-3.5-flash", + }); + assert.equal(resolved.supportsThinking, true); +}); + +test("reasoning_effort 'high' on gemini-3.5-flash is NOT rejected by routing policy", async () => { + await rulesDb.createReasoningRoutingRule(ruleInput()); + const decision = await policy.resolveReasoningRoutingRule({ + sourceModel: "gemini/gemini-3.5-flash", + sourceModelAliases: ["gemini-3.5-flash"], + sourceEffort: "high", + hasReasoningSignal: true, + }); + assert.ok(decision, "a matching rule must produce a decision"); + assert.equal(decision.capability, "supported"); +}); From 33e0fea8b047943a9c6b3ba0e52ce3b8b880b097 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Mon, 17 Aug 2026 16:49:46 +0800 Subject: [PATCH 03/22] fix(sse): flag OpenAI streams that close with content but no terminal marker (#10475) Issue #10443: when the upstream kills an SSE stream mid-generation (antigravity/Gemini does this under its own rate enforcement), OmniRoute closed the stream silently for OpenAI-format clients - HTTP 200, a few content chunks, no finish_reason. The client sees a truncated turn. resolveSilentCloseReason() only flagged that shape for Claude clients (#7699). Extend it to OpenAI chat completions guarded on sawContent(), and teach hasClientTerminalSseMarker() that a non-null finish_reason chunk is a terminal marker (some providers omit data: [DONE]). Every known OpenAI-producing path ends with one of the two, so content forwarded without either is an upstream drop and now surfaces the in-band 502 error chunk + [DONE] instead of a silent close. TDD: tests/unit/silent-sse-close-openai-10443.test.ts - core case RED before / GREEN after, plus guard cases for finish_reason-only close, [DONE] close, empty-content (#8649 verdict preserved), and literal finish_reason text inside model content (JSON escaping keeps the raw bytes from matching the unescaped-field regex). Signed-off-by: Minxi Hou --- open-sse/utils/streamHandler.ts | 26 ++- .../silent-sse-close-openai-10443.test.ts | 158 ++++++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 tests/unit/silent-sse-close-openai-10443.test.ts diff --git a/open-sse/utils/streamHandler.ts b/open-sse/utils/streamHandler.ts index f77c2ac539..4e7bf382dd 100644 --- a/open-sse/utils/streamHandler.ts +++ b/open-sse/utils/streamHandler.ts @@ -210,6 +210,14 @@ function hasClientTerminalSseMarker(text: string, clientResponseFormat?: string ); } + // OpenAI chat completions: some providers omit `data: [DONE]` (already + // matched above) and terminate with a finish_reason chunk instead. A + // non-null finish_reason value is that terminal signal — a bare + // `finish_reason: null` delta chunk must NOT count (#10443). + if (clientResponseFormat === FORMATS.OPENAI) { + return /"finish_reason"\s*:\s*"[^"]+"/.test(text); + } + return false; } @@ -516,8 +524,22 @@ function resolveSilentCloseReason(input: { }): string | null { if (!input.bytesWereForwarded) return null; - if (!input.clientTerminalSeen && input.clientResponseFormat === FORMATS.CLAUDE) { - return "Upstream stream ended without a terminal marker"; + if (!input.clientTerminalSeen) { + if (input.clientResponseFormat === FORMATS.CLAUDE) { + return "Upstream stream ended without a terminal marker"; + } + // #10443: every known path that produces OpenAI chat chunks emits a + // terminal — the response translators (gemini/claude/kiro/cursor-to-openai) + // all emit a finish_reason chunk, the non-standard executors (kiro, cursor, + // nlpcloud, poe-web, copilot-m365-web, chatgpt-web, chipotle, gitlab) + // enqueue `data: [DONE]` themselves, and standard OpenAI-compatible + // upstreams end with finish_reason + [DONE] per spec. So a close that + // forwarded content but no terminal marker is an upstream drop, not a + // legitimate end. Guard on sawContent() so the #8649 empty-content + // verdict below keeps its more precise shape for content-free closes. + if (input.clientResponseFormat === FORMATS.OPENAI && input.contentWatcher.sawContent()) { + return "Upstream stream ended without a terminal marker"; + } } const watcher = input.contentWatcher; diff --git a/tests/unit/silent-sse-close-openai-10443.test.ts b/tests/unit/silent-sse-close-openai-10443.test.ts new file mode 100644 index 0000000000..b27a9c3962 --- /dev/null +++ b/tests/unit/silent-sse-close-openai-10443.test.ts @@ -0,0 +1,158 @@ +/** + * Regression test for #10443 — silent SSE truncation on OpenAI chat completions. + * + * The reporter's symptom: HTTP 200, a few content chunks forwarded, then the + * upstream (antigravity/Gemini) drops the stream without a terminal marker — + * no finish_reason chunk, no `data: [DONE]`. OmniRoute used to close the stream + * silently, so OpenAI-compatible clients (Hermes) see a truncated stream with + * no finish_reason at all. + * + * #7699 fixed this shape for Claude-format clients only; the resolver + * deliberately returned null for every other format because "many formats have + * no [DONE] equivalent". That reasoning does not hold for OpenAI chat + * completions: a healthy OpenAI stream ALWAYS carries either a finish_reason + * chunk (translator/upstream) or `data: [DONE]`. So a close that forwarded + * content but no terminal marker is a failure there too, and must surface a + * synthetic error chunk instead of a silent close. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { createDisconnectAwareStream, createStreamController } = + await import("../../open-sse/utils/streamHandler.ts"); +const { FORMATS } = await import("../../open-sse/translator/formats.ts"); + +function createNoopAbortWritableStream(): { getWriter: () => { abort: () => Promise } } { + return { getWriter: () => ({ abort: () => Promise.resolve() }) }; +} + +async function drainStream(stream: ReadableStream): Promise { + const reader = stream.getReader(); + const parts: Uint8Array[] = []; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + parts.push(value); + } + return new TextDecoder().decode( + parts.reduce((acc, c) => { + const merged = new Uint8Array(acc.length + c.length); + merged.set(acc, 0); + merged.set(c, acc.length); + return merged; + }, new Uint8Array(0)) + ); +} + +/** + * Wire a synthetic upstream byte stream (already in client format) through + * createDisconnectAwareStream with the given clientResponseFormat and return + * everything the client would receive. + */ +async function runClientStream( + upstreamChunks: string[], + clientResponseFormat: string | null +): Promise { + const upstream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + for (const chunk of upstreamChunks) controller.enqueue(encoder.encode(chunk)); + controller.close(); + }, + }); + + const transform = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk); + }, + }); + const transformedBody = upstream.pipeThrough(transform); + + const sc = createStreamController({ + provider: "antigravity", + model: "gemini-3.6-flash-medium", + clientResponseFormat, + }); + + const wrapped = createDisconnectAwareStream( + { readable: transformedBody, writable: createNoopAbortWritableStream() }, + sc + ); + + return drainStream(wrapped); +} + +test("#10443 OpenAI format: content then bare close emits synthetic error, not a silent truncation", async () => { + const text = await runClientStream( + ['data: {"choices":[{"index":0,"delta":{"content":"partial"},"finish_reason":null}]}\n\n'], + FORMATS.OPENAI + ); + + // The forwarded content survives... + assert.match(text, /partial/); + // ...and the close must be flagged, OpenAI style: error chunk + [DONE]. + assert.match(text, /"finish_reason":\s*"error"/); + assert.match(text, /data: \[DONE\]/); + assert.match(text, /Upstream stream ended without a terminal marker/); +}); + +test("#10443 OpenAI format: finish_reason chunk counts as terminal, no synthetic error", async () => { + // Upstream that legitimately closes WITHOUT `data: [DONE]` but WITH a + // finish_reason chunk (some OpenAI-compatible providers do exactly this). + const text = await runClientStream( + [ + 'data: {"choices":[{"index":0,"delta":{"content":"partial"},"finish_reason":null}]}\n\n', + 'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', + ], + FORMATS.OPENAI + ); + + assert.match(text, /partial/); + assert.doesNotMatch(text, /Upstream stream ended without a terminal marker/); + assert.doesNotMatch(text, /"finish_reason":\s*"error"/); +}); + +test("#10443 OpenAI format: data: [DONE] close is unchanged, no synthetic error", async () => { + const text = await runClientStream( + [ + 'data: {"choices":[{"index":0,"delta":{"content":"partial"},"finish_reason":null}]}\n\n', + "data: [DONE]\n\n", + ], + FORMATS.OPENAI + ); + + assert.match(text, /partial/); + assert.doesNotMatch(text, /Upstream stream ended without a terminal marker/); + assert.doesNotMatch(text, /"finish_reason":\s*"error"/); +}); + +test("#10443 OpenAI format: content-free close keeps the #8649 empty-content verdict", async () => { + // An SSE frame with no content forwarded: the #8649 empty-content rule must + // keep its verdict (the no-marker rule only applies when content was forwarded). + const text = await runClientStream( + ['data: {"choices":[{"index":0,"delta":{},"finish_reason":null}]}\n\n'], + FORMATS.OPENAI + ); + + assert.match(text, /Provider returned empty content/); + assert.doesNotMatch(text, /Upstream stream ended without a terminal marker/); +}); + +test("#10443 OpenAI format: literal finish_reason text inside model content does not count as a terminal marker", async () => { + // The model itself outputs a JSON snippet containing `"finish_reason": "stop"`. + // JSON.stringify escapes the inner quotes, so the raw SSE bytes carry + // `\"finish_reason\": \"stop\"` inside delta.content — the terminal-marker + // regex requires an unescaped field and must NOT match it. If it did, the + // clientTerminalSeen flag would trip early and a real mid-stream drop after + // such content would be misread as a clean completion. + const text = await runClientStream( + [ + 'data: {"choices":[{"index":0,"delta":{"content":"a stream ends with \\"finish_reason\\": \\"stop\\""},"finish_reason":null}]}\n\n', + ], + FORMATS.OPENAI + ); + + // No real terminal marker was forwarded, so the close is still flagged. + assert.match(text, /Upstream stream ended without a terminal marker/); + assert.match(text, /"finish_reason":\s*"error"/); +}); From 9bfdc15cbcc1dac4e83a1b0325da0b9143226073 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 17 Aug 2026 05:49:58 -0300 Subject: [PATCH 04/22] fix(providers): emit Cursor kv_after_text before tool calls instead of truncating them (#10215) (#10502) Co-authored-by: adevwithpurpose --- .../10215-cursor-kv-after-text-toolcalls.md | 1 + open-sse/executors/cursor.ts | 25 +++++-- tests/unit/cursor-streaming.test.ts | 71 ++++++++++++++++++- 3 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 changelog.d/fixes/10215-cursor-kv-after-text-toolcalls.md diff --git a/changelog.d/fixes/10215-cursor-kv-after-text-toolcalls.md b/changelog.d/fixes/10215-cursor-kv-after-text-toolcalls.md new file mode 100644 index 0000000000..db0ea1df9a --- /dev/null +++ b/changelog.d/fixes/10215-cursor-kv-after-text-toolcalls.md @@ -0,0 +1 @@ +- **fix(cursor):** Stop truncating pending tool calls on non-composer models when a KV checkpoint arrives after text but before the `exec_mcp` frame — the KV short-circuit is now gated to the composer family where it was verified ([#10215](https://github.com/diegosouzapw/OmniRoute/issues/10215)). \ No newline at end of file diff --git a/open-sse/executors/cursor.ts b/open-sse/executors/cursor.ts index 42c7bf0055..89f7799b03 100644 --- a/open-sse/executors/cursor.ts +++ b/open-sse/executors/cursor.ts @@ -681,13 +681,26 @@ export function processFrame( // after text means the model finished and the server is saving the // turn. Phase 8 keeps both signals as defense-in-depth. // - // Safe vs tool calls: when the model invokes a tool, the exec_mcp event - // always arrives at or before this kv checkpoint (verified across many - // live composer-2.5 trials — a tool call never follows kv_after_text), so - // endReason is already "tool_calls" by the time we get here. Ending on - // kv_after_text therefore never truncates a pending tool call. + // Safe vs tool calls (composer family only): when the model invokes a + // tool, the exec_mcp event always arrives at or before this kv + // checkpoint (verified across many live composer-2.5 trials — a tool call + // never follows kv_after_text), so endReason is already "tool_calls" by + // the time we get here. Ending on kv_after_text therefore never truncates + // a pending tool call on composer. + // + // Non-composer models (cursor/grok-4.5-high, auto, ...) emit the KV + // checkpoint as a blob-store side-channel frame (envelope field 4, + // kv_get_blob/kv_set_blob) with NO turn-completion semantics, and it can + // arrive while the model is still streaming a long preamble BEFORE a + // pending exec_mcp. Ending the turn there drops that exec_mcp, leaving a + // narration-only finish_reason "stop" with zero tool_calls (#10215). On + // this family only the real terminal signals (turn_ended, + // tool_call_completed, server_end) decide — kvAfterTextSeen is kept purely + // as an observational flag, never as the turn terminator. ctx.kvAfterTextSeen = true; - ctx.endReason = "kv_after_text"; + if (isComposerModel(ctx.model)) { + ctx.endReason = "kv_after_text"; + } } } } diff --git a/tests/unit/cursor-streaming.test.ts b/tests/unit/cursor-streaming.test.ts index 068c723001..0eb26e684f 100644 --- a/tests/unit/cursor-streaming.test.ts +++ b/tests/unit/cursor-streaming.test.ts @@ -57,6 +57,23 @@ function buildKvServerMessagePayload(): Buffer { return lenPrefixed(4, Buffer.alloc(0)); } +// AgentServerMessage { exec_server_message (2): { id (1): 9, mcp_args (11): { tool_name (5): str } } } +function buildExecMcpPayload(): Buffer { + const mcpArgs = lenPrefixed(5, Buffer.from("magic_tool")); + const esm = Buffer.concat([tag(1, 0), v(9), lenPrefixed(11, mcpArgs)]); + return lenPrefixed(2, esm); +} + +// Faithful model of driveH2's per-frame endReason teardown (cursor.ts): after +// each decoded frame a truthy endReason detaches listeners and stops reading, +// so any frame still buffered after it is dropped. +function driveFrames(ctx: StreamCtx, frames: Buffer[]): void { + for (const f of frames) { + processFrame(f, ctx, new Set()); + if (ctx.endReason) return; + } +} + // JSON error payload (Connect-RPC error envelope) function buildJsonErrorPayload(): Buffer { return Buffer.from( @@ -117,14 +134,64 @@ test("processFrame accumulates token_delta", () => { assert.equal(ctx.tokenDelta, 55); }); -test("processFrame sets endReason on kv_server_message after text", () => { - const ctx = newStreamCtx("auto", () => {}); +test("processFrame sets endReason on kv_server_message after text for composer models", () => { + // Composer family keeps the plain-chat short-circuit: KV is the verified + // early end-of-turn signal and a tool call never follows kv_after_text. + const ctx = newStreamCtx("cursor/composer-2.5", () => {}); processFrame(buildTextDeltaPayload("hi"), ctx, new Set()); processFrame(buildKvServerMessagePayload(), ctx, new Set()); assert.equal(ctx.endReason, "kv_after_text"); assert.equal(ctx.kvAfterTextSeen, true); }); +test("processFrame does not end turn on kv_server_message for non-composer models", () => { + // Non-composer models (cursor/grok-4.5-high, auto) emit the KV checkpoint as + // a blob-store side-channel frame with no turn-completion semantics — it can + // arrive mid-stream before a pending exec_mcp. It must never terminate here; + // only the real terminal signals (turn_ended / tool_call_completed) decide. + for (const model of ["cursor/grok-4.5-high", "auto"]) { + const ctx = newStreamCtx(model, () => {}); + processFrame(buildTextDeltaPayload("hi"), ctx, new Set()); + processFrame(buildKvServerMessagePayload(), ctx, new Set()); + assert.equal(ctx.endReason, null, `model ${model} must not end on kv_after_text`); + assert.equal(ctx.kvAfterTextSeen, true, `model ${model} still observes the KV checkpoint`); + } +}); + +test("REGRESSION #10215: non-composer kv_after_text before exec_mcp must not drop the tool call", () => { + // text → kv_server_message → exec_mcp must still process the tool call: + // the KV checkpoint (with no turn semantics on this family) must not tear the + // frame loop down before the pending exec_mcp is decoded. Prior to the fix + // this left ctx.toolCalls=0 → finish_reason "stop" (narration-only truncation). + for (const model of ["cursor/grok-4.5-high", "auto"]) { + const ctx = newStreamCtx(model, () => {}); + driveFrames(ctx, [ + buildTextDeltaPayload("a long preamble before the tool call"), + buildKvServerMessagePayload(), + buildExecMcpPayload(), + ]); + assert.equal(ctx.toolCalls.length, 1, `model ${model} must keep the pending tool call`); + assert.equal(ctx.endReason, "tool_calls", `model ${model} ends on the real tool signal`); + assert.equal(ctx.kvAfterTextSeen, true); + } +}); + +test("REGRESSION #10215: long preamble (>2.5K chars) then KV then exec_mcp keeps the tool call", () => { + // Covers the at-risk band the reporter identified (2505-2933 chars of text + // before the tool call on cursor/grok-4.5-high). A KV checkpoint arriving + // mid-preamble must not truncate the still-pending exec_mcp. + const longPreamble = + "The model streams a lengthy preamble before invoking a tool. ".repeat(60); + assert.ok(longPreamble.length > 2500); + for (const model of ["cursor/grok-4.5-high", "auto"]) { + const ctx = newStreamCtx(model, () => {}); + driveFrames(ctx, [buildTextDeltaPayload(longPreamble), buildKvServerMessagePayload(), buildExecMcpPayload()]); + assert.equal(ctx.toolCalls.length, 1, `model ${model} must keep the tool call`); + assert.equal(ctx.endReason, "tool_calls"); + assert.ok(ctx.totalText.length > 2500); + } +}); + test("buildCursorUsage degrades to prompt-only counts for an empty response", () => { // emitUsage now always emits on the success path (OpenAI streaming contract), // relying on buildCursorUsage producing a valid usage object even when the From bdc30ca4ddc31f67c3f691ffb44c13c18738d599 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 17 Aug 2026 05:50:09 -0300 Subject: [PATCH 05/22] fix(providers): strip uniqueItems from Gemini tool schemas to avoid upstream 400 (#9617) (#10511) Co-authored-by: adevwithpurpose --- .../fixes/9617-gemini-uniqueitems-strip.md | 1 + open-sse/translator/helpers/geminiHelper.ts | 5 ++ tests/unit/9617-gemini-uniqueitems.test.ts | 77 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 changelog.d/fixes/9617-gemini-uniqueitems-strip.md create mode 100644 tests/unit/9617-gemini-uniqueitems.test.ts diff --git a/changelog.d/fixes/9617-gemini-uniqueitems-strip.md b/changelog.d/fixes/9617-gemini-uniqueitems-strip.md new file mode 100644 index 0000000000..8e01e17a28 --- /dev/null +++ b/changelog.d/fixes/9617-gemini-uniqueitems-strip.md @@ -0,0 +1 @@ +- fix(providers): strip uniqueItems from Gemini tool schemas (Gemini rejects it with 400 'Unknown name uniqueItems') (#9617) diff --git a/open-sse/translator/helpers/geminiHelper.ts b/open-sse/translator/helpers/geminiHelper.ts index 5d7bfc676e..623e39cba3 100644 --- a/open-sse/translator/helpers/geminiHelper.ts +++ b/open-sse/translator/helpers/geminiHelper.ts @@ -58,6 +58,11 @@ export const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([ "contains", "minContains", "maxContains", + // #9617: array uniqueness keyword — agentic-CLI tool schemas (JSON-Schema + // generators) set this routinely and Gemini's schema parser has no field for + // it, rejecting the whole request with "Unknown name \"uniqueItems\"". + // Upstream 9router already strips it alongside `contains` for the same error. + "uniqueItems", // Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf) "anyOf", "oneOf", diff --git a/tests/unit/9617-gemini-uniqueitems.test.ts b/tests/unit/9617-gemini-uniqueitems.test.ts new file mode 100644 index 0000000000..287bbc564d --- /dev/null +++ b/tests/unit/9617-gemini-uniqueitems.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { buildGeminiTools } from "../../open-sse/translator/helpers/geminiToolsSanitizer.ts"; + +// Issue #9617: Gemini rejects `uniqueItems` in function_declarations parameter schemas +// with HTTP 400 "Unknown name \"uniqueItems\" ... Cannot find field" (Gemini's protobuf-JSON +// schema parser only accepts a subset of JSON Schema/OpenAPI 3.0 — the same class of error +// already fixed for `multipleOf`, `minItems`, `maxItems`, `strict`, `encrypted` in +// GEMINI_UNSUPPORTED_SCHEMA_KEYS, open-sse/translator/helpers/geminiHelper.ts). +test("buildGeminiTools strips uniqueItems from array schemas (issue #9617)", () => { + const tools = [ + { + type: "function", + function: { + name: "exit_worktree", + description: "test tool with an array-of-objects parameter", + parameters: { + type: "object", + properties: { + items: { + type: "array", + uniqueItems: true, + items: { + type: "object", + properties: { + name: { type: "string" }, + action: { type: "string" }, + }, + required: ["name", "action"], + }, + }, + }, + required: ["items"], + }, + }, + }, + ]; + + const geminiTools = buildGeminiTools(tools); + const serialized = JSON.stringify(geminiTools); + + assert.ok(geminiTools, "expected buildGeminiTools to return a tools array"); + assert.equal( + serialized.includes("uniqueItems"), + false, + `uniqueItems leaked into the Gemini payload (would trigger upstream 400 "Unknown name \\"uniqueItems\\""): ${serialized}` + ); +}); + +// Companion: a top-level (non-nested) array property with uniqueItems is also stripped — +// matches the reporter's deeply-nested case with extra path coverage. +test("buildGeminiTools strips uniqueItems from a top-level array parameter schema (issue #9617)", () => { + const tools = [ + { + type: "function", + function: { + name: "list_worktrees", + description: "test tool with a top-level array parameter", + parameters: { + type: "object", + properties: { + paths: { + type: "array", + uniqueItems: true, + items: { type: "string" }, + }, + }, + required: ["paths"], + }, + }, + }, + ]; + + const serialized = JSON.stringify(buildGeminiTools(tools)); + assert.equal(serialized.includes("uniqueItems"), false); +}); \ No newline at end of file From 2723698fe230ba9444d274b97c4b06a824f4ae78 Mon Sep 17 00:00:00 2001 From: Aman <1402357+Zartharas@users.noreply.github.com> Date: Mon, 17 Aug 2026 02:50:21 -0600 Subject: [PATCH 06/22] fix(providers): update token-backed web sessions (#10518) --- .../10518-token-backed-web-session-update.md | 1 + src/app/api/providers/[id]/route.ts | 3 ++- src/shared/providers/webSessionCredentials.ts | 6 +++++ tests/unit/bulk-web-session-import.test.ts | 22 +++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/10518-token-backed-web-session-update.md diff --git a/changelog.d/fixes/10518-token-backed-web-session-update.md b/changelog.d/fixes/10518-token-backed-web-session-update.md new file mode 100644 index 0000000000..78ca1793b8 --- /dev/null +++ b/changelog.d/fixes/10518-token-backed-web-session-update.md @@ -0,0 +1 @@ +- **fix(providers):** allow token-backed web sessions stored with `authType: "cookie"` to refresh their token through the provider update API ([#10518](https://github.com/diegosouzapw/OmniRoute/pull/10518)) — thanks @Zartharas diff --git a/src/app/api/providers/[id]/route.ts b/src/app/api/providers/[id]/route.ts index 38c1746cc6..adb5840fa0 100644 --- a/src/app/api/providers/[id]/route.ts +++ b/src/app/api/providers/[id]/route.ts @@ -25,6 +25,7 @@ import { import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { isApiKeyRevealEnabled, maskStoredApiKey } from "@/lib/apiKeyExposure"; import { cleanupProviderModelsAfterConnectionDelete } from "@/lib/db/models"; +import { canUpdateProviderApiKey } from "@/shared/providers/webSessionCredentials"; import { refreshConnectionRateLimits, enableRateLimitProtection, @@ -161,7 +162,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id: if (globalPriority !== undefined) updateData.globalPriority = globalPriority; if (defaultModel !== undefined) updateData.defaultModel = defaultModel; if (isActive !== undefined) updateData.isActive = isActive; - if (apiKey && existing.authType === "apikey") { + if (apiKey && canUpdateProviderApiKey(existing.authType, existing.provider)) { if (existing.provider === "chatgpt-web-codex") { const validationId = incomingPsd && typeof incomingPsd.validationId === "string" diff --git a/src/shared/providers/webSessionCredentials.ts b/src/shared/providers/webSessionCredentials.ts index 3557dbf08b..77b64c0e17 100644 --- a/src/shared/providers/webSessionCredentials.ts +++ b/src/shared/providers/webSessionCredentials.ts @@ -375,6 +375,12 @@ export function getWebSessionCredentialRequirement( ); } +export function canUpdateProviderApiKey(authType: unknown, providerId: unknown): boolean { + if (authType === "apikey") return true; + if (authType !== "cookie") return false; + return getWebSessionCredentialRequirement(providerId)?.kind === "token"; +} + export function requiresWebSessionCredential(providerId: unknown): boolean { const requirement = getWebSessionCredentialRequirement(providerId); return !!requirement && requirement.kind !== "none"; diff --git a/tests/unit/bulk-web-session-import.test.ts b/tests/unit/bulk-web-session-import.test.ts index 1eaea2c049..ab426f83e8 100644 --- a/tests/unit/bulk-web-session-import.test.ts +++ b/tests/unit/bulk-web-session-import.test.ts @@ -10,6 +10,7 @@ import { bulkWebSessionImportSchema } from "../../src/shared/validation/schemas. import { requiresWebSessionCredential, getWebSessionCredentialRequirement, + canUpdateProviderApiKey, hasUsableWebSessionCredential, resolveWebSessionImportApiKey, } from "../../src/shared/providers/webSessionCredentials.ts"; @@ -160,6 +161,27 @@ describe("web-session credential helpers", () => { }); }); +describe("canUpdateProviderApiKey", () => { + it("preserves normal API-key credential updates", () => { + assert.equal(canUpdateProviderApiKey("apikey", "openai"), true); + }); + + it("allows token-kind web sessions stored with cookie authType", () => { + assert.equal(canUpdateProviderApiKey("cookie", "deepseek-web"), true); + assert.equal(canUpdateProviderApiKey("cookie", "zai-web"), true); + }); + + it("does not allow cookie-kind web sessions to update apiKey", () => { + assert.equal(canUpdateProviderApiKey("cookie", "chatgpt-web"), false); + assert.equal(canUpdateProviderApiKey("cookie", "claude-web"), false); + }); + + it("does not broaden non-cookie auth types", () => { + assert.equal(canUpdateProviderApiKey("oauth", "deepseek-web"), false); + assert.equal(canUpdateProviderApiKey(null, "deepseek-web"), false); + }); +}); + describe("resolveWebSessionImportApiKey (token-kind imports must populate apiKey)", () => { // Regression: the bulk web-session import stored token-kind credentials // (deepseek-web, copilot-web, t3-chat-web, …) only in providerSpecificData and From b082d0735bd6a7b3bacbebd08cb451c5e7497825 Mon Sep 17 00:00:00 2001 From: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:00:05 +0800 Subject: [PATCH 07/22] fix(api-manager): allow empty combo restrictions (#10066) * fix(api-manager): allow empty combo restrictions Represent unrestricted Combo access explicitly as combo/* so an empty Allowed Combos list can deny every Combo without affecting direct model routes. Preserve existing keys through migration 149 and cover Dashboard, policy, routing-target, and migration behavior. * docs: sync migration count to 149 after api-key combo-access migration Merging release/v3.8.50 forward landed 149_api_key_combo_access.sql, bumping the real migration count from 148 to 149. Updates README.md, AGENTS.md, llm.txt (root + all 42 i18n mirrors, exact-copy requirement) so the strict docs-counts-sync gate matches the live count again. Co-authored-by: diegosouzapw --------- Co-authored-by: adevwithpurpose Co-authored-by: xz-dev Co-authored-by: diegosouzapw --- AGENTS.md | 2 +- README.md | 2 +- .../api-manager-empty-combo-allowlist.md | 1 + docs/i18n/ar/llm.txt | 8 +- docs/i18n/az/llm.txt | 8 +- docs/i18n/bg/llm.txt | 8 +- docs/i18n/bn/llm.txt | 8 +- docs/i18n/cs/llm.txt | 8 +- docs/i18n/da/llm.txt | 8 +- docs/i18n/de/llm.txt | 8 +- docs/i18n/es/llm.txt | 8 +- docs/i18n/fa/llm.txt | 8 +- docs/i18n/fi/llm.txt | 8 +- docs/i18n/fr/llm.txt | 8 +- docs/i18n/gu/llm.txt | 8 +- docs/i18n/he/llm.txt | 8 +- docs/i18n/hi/llm.txt | 8 +- docs/i18n/hu/llm.txt | 8 +- docs/i18n/id/llm.txt | 8 +- docs/i18n/in/llm.txt | 8 +- docs/i18n/it/llm.txt | 8 +- docs/i18n/ja/llm.txt | 8 +- docs/i18n/ko/llm.txt | 8 +- docs/i18n/mr/llm.txt | 8 +- docs/i18n/ms/llm.txt | 8 +- docs/i18n/nl/llm.txt | 8 +- docs/i18n/no/llm.txt | 8 +- docs/i18n/phi/llm.txt | 8 +- docs/i18n/pl/llm.txt | 8 +- docs/i18n/pt-BR/llm.txt | 8 +- docs/i18n/pt/llm.txt | 8 +- docs/i18n/ro/llm.txt | 8 +- docs/i18n/ru/llm.txt | 8 +- docs/i18n/sk/llm.txt | 8 +- docs/i18n/sv/llm.txt | 8 +- docs/i18n/sw/llm.txt | 8 +- docs/i18n/ta/llm.txt | 8 +- docs/i18n/te/llm.txt | 8 +- docs/i18n/th/llm.txt | 8 +- docs/i18n/tr/llm.txt | 8 +- docs/i18n/uk-UA/llm.txt | 8 +- docs/i18n/ur/llm.txt | 8 +- docs/i18n/vi/llm.txt | 8 +- docs/i18n/zh-CN/llm.txt | 8 +- docs/i18n/zh-TW/llm.txt | 8 +- llm.txt | 8 +- scripts/check/check-migration-numbering.mjs | 19 ++-- .../api-manager/ApiManagerPageClient.tsx | 14 ++- src/lib/db/apiKeys.ts | 10 +- .../migrations/149_api_key_combo_access.sql | 17 ++++ src/shared/constants/comboAccess.ts | 1 + src/shared/utils/apiKeyPolicy.ts | 6 +- tests/e2e/api-keys-flow.spec.ts | 91 +++++++++++++++++++ tests/unit/api-key-policy.test.ts | 74 +++++++++++++++ tests/unit/api-manager-page-static.test.ts | 20 ++++ tests/unit/check-migration-numbering.test.ts | 3 +- ...migration-149-api-key-combo-access.test.ts | 46 ++++++++++ 57 files changed, 458 insertions(+), 192 deletions(-) create mode 100644 changelog.d/fixes/api-manager-empty-combo-allowlist.md create mode 100644 src/lib/db/migrations/149_api_key_combo_access.sql create mode 100644 src/shared/constants/comboAccess.ts create mode 100644 tests/unit/migration-149-api-key-combo-access.test.ts diff --git a/AGENTS.md b/AGENTS.md index 398409e694..71f7e72ee5 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 (148 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (149 migrations) | | Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic | | MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes | | A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | diff --git a/README.md b/README.md index 582b359b38..ba0cf4d2ba 100644 --- a/README.md +++ b/README.md @@ -1108,7 +1108,7 @@ same process on one port, so there is no separate CLI-only package today. RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 117 domain modules, 148 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 117 domain modules, 149 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) diff --git a/changelog.d/fixes/api-manager-empty-combo-allowlist.md b/changelog.d/fixes/api-manager-empty-combo-allowlist.md new file mode 100644 index 0000000000..7180578281 --- /dev/null +++ b/changelog.d/fixes/api-manager-empty-combo-allowlist.md @@ -0,0 +1 @@ +- **fix(api-manager):** Allowed Combos can now be restricted to zero entries: **All** is stored explicitly as `combo/*`, while **Restrict** with no selection saves an empty allowlist that denies Combo routes without blocking direct models. Existing keys are migrated to preserve their previous allow-all behavior. diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index f641126792..e8d80bc432 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 f6e5e38f32..9807246648 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 f6e5e38f32..9807246648 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 5805db9978..ca40a2af33 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 c27ab7ccbd..0140862a6f 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 1ed4cf415c..a3cf8954e5 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 8de02ddf4a..bfaa7ecebe 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 741e735beb..6b3f59b505 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 ceab9960f4..65aa080157 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 8e80e7c7c9..462f54c154 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 4990613faf..f8d9f7f3fa 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 12adedeb1d..c43c20de29 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 ff8db94c3c..cf9f1483cf 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 e0fa66c5bf..77156c44a0 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 674271baea..4050fad047 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 3bbaeba2cc..aedf870577 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 33ccf7ac98..1e02400b02 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 2d80f933e0..3ad75feb38 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 3715b9e998..f15482dcec 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 dc2d0b7244..121b3434cb 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 a9912224a7..ff8c12d24a 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 a4e8a2b23b..bfed0ee1e1 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 94fc04a3c8..d830f957bd 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 75ae6d1792..f56e7c2b5c 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 3073856302..a3abd84348 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 0905afcc8c..53e0138fe6 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 558d49fb12..cb237dd463 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 31078a5518..490aaea9d3 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 a9ad0860ca..1edff8f8b4 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 75eb326bb1..6e3477c81d 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 ff2b679b45..b50c0c51fb 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 8ed3e7cd38..fc1079651f 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 f1d03f9630..8469b9297b 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 55eebb6b10..cf67ba5b5b 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 3287e82ca6..ac34be71cf 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 9061f8ed2d..ed8d9d4f72 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 77dedad4b4..fe93cdd55e 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 f042cf63ae..cb85acdff7 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 1cd242b06d..1b1fb35fb0 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 b51a882cbe..d0e3d8f389 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 371bdd4c96..90b74a547d 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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 2e625dd2ba..443a9a724f 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -394,7 +394,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/llm.txt b/llm.txt index 3594ab34bc..a8b57b8c26 100644 --- a/llm.txt +++ b/llm.txt @@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 148 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 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/ # 148 versioned SQL migration files +│ │ │ └── migrations/ # 149 versioned SQL migration files │ │ ├── evals/ # Eval runner and scheduler │ │ ├── memory/ # Persistent conversational memory │ │ │ ├── extraction.ts # Memory extraction from conversations @@ -390,7 +390,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 117 `src/lib/db/` modules with 148 SQL migrations. +9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 117 `src/lib/db/` modules with 149 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`. @@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 148 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 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/scripts/check/check-migration-numbering.mjs b/scripts/check/check-migration-numbering.mjs index 492e10c3d1..b377c1af74 100644 --- a/scripts/check/check-migration-numbering.mjs +++ b/scripts/check/check-migration-numbering.mjs @@ -43,14 +43,19 @@ export const KNOWN_DUPLICATE_VERSIONS = new Set([ // --------------------------------------------------------------------------- // ALLOWLIST 2 — gaps de sequência CONHECIDOS. // Fonte: auditoria do disco (src/lib/db/migrations/). Além dos slots legados, -// As migrations Radar 144–145 e a migration 143 já aterrissaram. O job registry -// foi promovido de 139 para 146 pela tabela RENAMED_MIGRATION_COMPATIBILITY. A -// 147–149 estão reservadas por migrations atualmente em trânsito nos PRs #8228, -// #9313, #10047 e #10066; esta branch usa 150 para evitar essas colisões conhecidas. -// O stale-enforcement exige que cada reserva seja removida quando os arquivos -// correspondentes aterrissarem na release. +// As migrations Radar 144–145, a migration 143 e a 147 já aterrissaram. O job +// registry foi promovido de 139 para 146 pela tabela +// RENAMED_MIGRATION_COMPATIBILITY. A 149 aterrissa junto com #10066 +// (149_api_key_combo_access.sql). 148 permanece reservada por PRs #10001 e +// #10047 ainda em trânsito. O stale-enforcement exige que cada reserva seja +// removida quando os arquivos correspondentes aterrissarem na release. // --------------------------------------------------------------------------- -export const KNOWN_GAPS = new Set(["026", "055", "121", "148", "149"]); // 121: número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12) +export const KNOWN_GAPS = new Set([ + "026", + "055", + "121", // número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12) + "148", // reserved by open PRs #10001 and #10047 +]); function pad3(n) { return String(n).padStart(3, "0"); diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index ddcb22f911..1e6d1db37a 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -33,6 +33,7 @@ import { BypassProviderQuotaToggle } from "./components/BypassProviderQuotaToggl import { ApiKeyCompressionToggle } from "./components/ApiKeyCompressionToggle"; import ProviderModelPermissionList from "./components/ProviderModelPermissionList"; import ReasoningRoutingRules from "@/shared/components/ReasoningRoutingRules"; +import { ALL_COMBOS_ACCESS_RULE } from "@/shared/constants/comboAccess"; // Constants for validation const MAX_KEY_NAME_LENGTH = 200; @@ -1056,7 +1057,8 @@ export default function ApiManagerPageClient() { const providerCount = providerWildcards.length; const modelCount = exactModels.length; const hasComboRestrictions = - Array.isArray(key.allowedCombos) && key.allowedCombos.length > 0; + Array.isArray(key.allowedCombos) && + !key.allowedCombos.includes(ALL_COMBOS_ACCESS_RULE); const hasConnectionRestrictions = Array.isArray(key.allowedConnections) && key.allowedConnections.length > 0; const noLogEnabled = key.noLog === true; @@ -1686,7 +1688,9 @@ const PermissionsModal = memo(function PermissionsModal({ () => (Array.isArray(apiKey?.blockedModels) ? apiKey.blockedModels : []), [apiKey?.blockedModels] ); - const initialCombos = Array.isArray(apiKey?.allowedCombos) ? apiKey.allowedCombos : []; + const initialCombos = Array.isArray(apiKey?.allowedCombos) + ? apiKey.allowedCombos.filter((combo) => combo !== ALL_COMBOS_ACCESS_RULE) + : []; const initialConnections = Array.isArray(apiKey?.allowedConnections) ? apiKey.allowedConnections : []; @@ -1702,7 +1706,9 @@ const PermissionsModal = memo(function PermissionsModal({ const [allowAll, setAllowAll] = useState( apiKey?.modelAccessMode === "restricted" ? false : initialModels.length === 0 ); - const [allowAllCombos, setAllowAllCombos] = useState(initialCombos.length === 0); + const [allowAllCombos, setAllowAllCombos] = useState( + apiKey?.allowedCombos?.includes(ALL_COMBOS_ACCESS_RULE) === true + ); const [noLogEnabled, setNoLogEnabled] = useState(apiKey?.noLog === true); const [autoResolveEnabled, setAutoResolveEnabled] = useState(apiKey?.autoResolve === true); const [keyIsActive, setKeyIsActive] = useState(apiKey?.isActive !== false); @@ -1938,7 +1944,7 @@ const PermissionsModal = memo(function PermissionsModal({ onSave( keyName, modelAccess.allowedModels, - allowAllCombos ? [] : selectedCombos, + allowAllCombos ? [ALL_COMBOS_ACCESS_RULE] : selectedCombos, noLogEnabled, allowAllConnections ? [] : selectedConnections, autoResolveEnabled, diff --git a/src/lib/db/apiKeys.ts b/src/lib/db/apiKeys.ts index 29439b38b4..55d0dde43a 100644 --- a/src/lib/db/apiKeys.ts +++ b/src/lib/db/apiKeys.ts @@ -30,6 +30,7 @@ import { hasClaudeCodeWildcardPermission, matchesWildcardPattern, } from "./apiKeys/modelPermissions"; +import { ALL_COMBOS_ACCESS_RULE } from "@/shared/constants/comboAccess"; import { parseAllowedModels, parseAllowedCombos, @@ -422,7 +423,7 @@ function getPreparedStatements(db: ApiKeysDbLike): ApiKeysStatements { "SELECT id, name, machine_id, model_access_mode, allowed_models, blocked_models, allowed_combos, allowed_connections, allowed_quotas, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute, throttle_delay_ms, max_sessions, revoked_at, expires_at, ip_allowlist, scopes, rate_limits, is_banned, key_hash, allowed_endpoints, stream_default_mode, cache_default_mode, disable_non_public_models, allow_usage_command, usage_limit_enabled, daily_usage_limit_usd, weekly_usage_limit_usd, chaos_mode_enabled, compression_enabled, proxy_id FROM api_keys WHERE key = ? OR key_hash = ?", ); _stmtInsertKey = db.prepare( - "INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at, key_prefix, key_hash, scopes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO api_keys (id, name, key, machine_id, allowed_models, allowed_combos, no_log, created_at, key_prefix, key_hash, scopes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ); _stmtDeleteKey = db.prepare("DELETE FROM api_keys WHERE id = ?"); } @@ -642,7 +643,7 @@ export async function createApiKey(name: string, machineId: string, scopes: stri machineId: machineId, modelAccessMode: "all" as const, allowedModels: [], // Empty array means all models allowed - allowedCombos: [], // Empty array means no explicit combo restriction + allowedCombos: [ALL_COMBOS_ACCESS_RULE], // Explicit wildcard means all combos allowed allowedConnections: [], // Empty array means all connections allowed noLog: false, allowUsageCommand: false, @@ -657,6 +658,7 @@ export async function createApiKey(name: string, machineId: string, scopes: stri apiKey.key, apiKey.machineId, "[]", + JSON.stringify(apiKey.allowedCombos), 0, apiKey.createdAt, apiKey.key.slice(0, 12), @@ -807,7 +809,7 @@ export async function updateApiKeyPermissions( } if (normalized.allowedCombos !== undefined) { - // Empty array means no explicit combo restriction; legacy allowed_models rules still apply. + // Empty array denies all combos; combo/* explicitly allows all combos. updates.push("allowed_combos = @allowedCombos"); params.allowedCombos = JSON.stringify(normalized.allowedCombos || []); } @@ -1269,7 +1271,7 @@ export async function getApiKeyMetadata( modelAccessMode: "all", allowedModels: [], blockedModels: [], - allowedCombos: [], + allowedCombos: [ALL_COMBOS_ACCESS_RULE], allowedConnections: [], allowedQuotas: [], noLog: false, diff --git a/src/lib/db/migrations/149_api_key_combo_access.sql b/src/lib/db/migrations/149_api_key_combo_access.sql new file mode 100644 index 0000000000..8ad76e1ff5 --- /dev/null +++ b/src/lib/db/migrations/149_api_key_combo_access.sql @@ -0,0 +1,17 @@ +-- 149: Make API-key Combo access explicit: combo/* allows all; [] denies all. +-- Existing null/empty/malformed values meant allow-all before this migration. + +UPDATE api_keys +SET allowed_combos = json_array('combo/*') +WHERE allowed_combos IS NULL + OR trim(allowed_combos) = '' + OR json_valid(allowed_combos) = 0 + OR CASE + WHEN json_valid(allowed_combos) = 1 THEN json_type(allowed_combos) != 'array' + ELSE 0 + END + OR CASE + WHEN json_valid(allowed_combos) = 1 AND json_type(allowed_combos) = 'array' + THEN json_array_length(allowed_combos) = 0 + ELSE 0 + END; diff --git a/src/shared/constants/comboAccess.ts b/src/shared/constants/comboAccess.ts new file mode 100644 index 0000000000..31a6383c2f --- /dev/null +++ b/src/shared/constants/comboAccess.ts @@ -0,0 +1 @@ +export const ALL_COMBOS_ACCESS_RULE = "combo/*"; diff --git a/src/shared/utils/apiKeyPolicy.ts b/src/shared/utils/apiKeyPolicy.ts index 99ca41c192..7df8f1cd1d 100644 --- a/src/shared/utils/apiKeyPolicy.ts +++ b/src/shared/utils/apiKeyPolicy.ts @@ -31,6 +31,7 @@ import { resolveEndpointCategory } from "@/shared/constants/endpointCategories"; import { resolveQuotaKeyScope } from "@/lib/quota/quotaKey"; import { isQuotaModelName, parseQuotaModelName } from "@/lib/quota/quotaModelNaming"; import { buildApiKeyUsageLimitPolicyRejection } from "@/lib/usage/apiKeyUsageLimits"; +import { ALL_COMBOS_ACCESS_RULE } from "@/shared/constants/comboAccess"; // Default to no per-key request cap. API keys can still opt into explicit // limits via Settings/API Keys, while provider/account quota controls remain @@ -181,6 +182,7 @@ function normalizeComboAccessName(value: unknown): string | null { } function matchesComboAccessRule(comboName: string, requestedModel: string, rule: string): boolean { + if (rule === ALL_COMBOS_ACCESS_RULE) return true; const normalizedRule = normalizeComboAccessName(rule); if (!normalizedRule) return false; return ( @@ -303,7 +305,7 @@ async function validateStandardRoutingTarget( modelStr: string ): Promise { let requestedComboName: string | null = null; - if (apiKeyInfo.allowedCombos && apiKeyInfo.allowedCombos.length > 0) { + if (Array.isArray(apiKeyInfo.allowedCombos)) { try { const comboAccess = await isComboAllowedForKey(apiKeyInfo.allowedCombos, modelStr); requestedComboName = comboAccess.comboName; @@ -557,7 +559,7 @@ async function validateComboAccess( allowedCombos: string[] | undefined, modelStr: string ): Promise<{ comboName: string | null; rejection: Response | null }> { - if (!allowedCombos?.length) return { comboName: null, rejection: null }; + if (!Array.isArray(allowedCombos)) return { comboName: null, rejection: null }; try { const comboAccess = await isComboAllowedForKey(allowedCombos, modelStr); if (comboAccess.allowed) return { comboName: comboAccess.comboName, rejection: null }; diff --git a/tests/e2e/api-keys-flow.spec.ts b/tests/e2e/api-keys-flow.spec.ts index d69863520f..b936970be5 100644 --- a/tests/e2e/api-keys-flow.spec.ts +++ b/tests/e2e/api-keys-flow.spec.ts @@ -10,6 +10,7 @@ type ApiKeyRecord = { key: string; fullKey: string; allowedModels: string[] | null; + allowedCombos: string[] | null; allowedConnections: string[] | null; /** Public shape: "all" | "restricted". Absent on legacy keys. */ modelAccessMode?: "all" | "restricted" | null; @@ -164,6 +165,7 @@ test.describe("API keys flow", () => { key: maskedKey, fullKey, allowedModels: null, + allowedCombos: ["combo/*"], allowedConnections: null, createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(), }); @@ -306,6 +308,7 @@ test.describe("API keys flow", () => { key: maskedKey, fullKey, allowedModels: null, + allowedCombos: ["combo/*"], allowedConnections: null, createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(), }); @@ -384,6 +387,92 @@ test.describe("API keys flow", () => { await expect(page.getByText("Renamed Key")).toBeVisible(); }); + test("saves Restrict with no allowed Combos", async ({ page }) => { + const state = { + key: { + id: "key-combo-restricted", + name: "No Combos Key", + key: "sk-live-****combo", + fullKey: "sk-live-combo-secret", + allowedModels: null, + allowedCombos: ["combo/*"], + allowedConnections: null, + createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(), + } satisfies ApiKeyRecord, + patchPayload: null as Record | null, + }; + + await page.route("**/v1/models", async (route) => { + await fulfillJson(route, { data: [] }); + }); + await page.route("**/api/settings", async (route) => { + await fulfillJson(route, {}); + }); + await page.route("**/api/providers", async (route) => { + await fulfillJson(route, { connections: [] }); + }); + await page.route("**/api/combos", async (route) => { + await fulfillJson(route, { + combos: [{ id: "combo-fast", name: "fast-chat", models: ["openai/gpt-4.1"] }], + }); + }); + await page.route(/\/api\/usage\/call-logs(?:\?.*)?$/, async (route) => { + await fulfillJson(route, []); + }); + await page.route("**/api/sessions", async (route) => { + await fulfillJson(route, { byApiKey: {} }); + }); + await page.route(/\/api\/keys\/key-combo-restricted$/, async (route) => { + if (route.request().method() !== "PATCH") { + await fulfillJson(route, { error: "Method not allowed" }, 405); + return; + } + state.patchPayload = (await route.request().postDataJSON()) as Record; + state.key.allowedCombos = state.patchPayload.allowedCombos as string[]; + await fulfillJson(route, { + message: "API key settings updated successfully", + ...state.patchPayload, + }); + }); + await page.route("**/api/keys", async (route) => { + await fulfillJson(route, { + keys: [{ ...state.key, fullKey: undefined }], + allowKeyReveal: true, + }); + }); + + await gotoDashboardRoute(page, "/dashboard/api-manager", { + timeoutMs: NAVIGATION_TIMEOUT_MS, + }); + await waitForPageToSettle(page); + await waitForNextDevCompileToFinish(page); + + const keyRow = page + .locator("div") + .filter({ has: page.getByText("No Combos Key", { exact: true }) }) + .first(); + await expect(keyRow.getByText("1 combos", { exact: true })).toHaveCount(0); + await keyRow.locator('button[title="Edit permissions"]').click({ force: true }); + + const permissionsDialog = page.getByRole("dialog", { + name: /permissions: no combos key/i, + }); + await expect(permissionsDialog).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); + await permissionsDialog + .getByRole("button", { name: /restrict/i }) + .nth(1) + .click(); + await expect(permissionsDialog.getByText(/restricted to 0 combos/i)).toBeVisible(); + + await permissionsDialog.getByRole("button", { name: /save permissions/i }).click(); + + await expect.poll(() => state.patchPayload?.allowedCombos).toEqual([]); + await expect(permissionsDialog).not.toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); + await expect(page.getByRole("button", { name: /0 combos/i })).toBeVisible({ + timeout: UI_STABILITY_TIMEOUT_MS, + }); + }); + test("validation error appears inside the create key modal, not behind the backdrop", async ({ page, }) => { @@ -430,6 +519,7 @@ test.describe("API keys flow", () => { key: maskedKey, fullKey, allowedModels: null, + allowedCombos: ["combo/*"], allowedConnections: null, createdAt: new Date().toISOString(), }); @@ -567,6 +657,7 @@ test.describe("API keys flow", () => { key: maskedKey, fullKey, allowedModels: null, + allowedCombos: ["combo/*"], allowedConnections: null, createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(), }); diff --git a/tests/unit/api-key-policy.test.ts b/tests/unit/api-key-policy.test.ts index 9f5c304006..ceda6e7348 100644 --- a/tests/unit/api-key-policy.test.ts +++ b/tests/unit/api-key-policy.test.ts @@ -607,6 +607,80 @@ test("enforceApiKeyPolicy enforces combo allowlists separately from model allowl assert.equal(mapped.rejection, null); }); +test("new API keys allow all Combos explicitly", async () => { + const key = await apiKeysDb.createApiKey("Explicit Combo Default", "machine-607"); + const stored = await apiKeysDb.getApiKeyMetadata(key.key); + + assert.deepEqual(stored?.allowedCombos, ["combo/*"]); +}); + +test("enforceApiKeyPolicy treats combo wildcard, empty list, and names as distinct access rules", async () => { + const allowAllKey = await createKeyWithPolicy({ allowedCombos: ["combo/*"] }); + const denyAllKey = await createKeyWithPolicy({ allowedCombos: [] }); + const allowNamedKey = await createKeyWithPolicy({ allowedCombos: ["fast-chat"] }); + await combosDb.createCombo({ + name: "fast-chat", + strategy: "priority", + models: ["openai/gpt-4.1"], + }); + await combosDb.createCombo({ + name: "slow-chat", + strategy: "priority", + models: ["anthropic/claude-3-5-sonnet"], + }); + const policy = await loadPolicy("combo-access-modes"); + + const allowAll = await policy.enforceApiKeyPolicy( + makePolicyRequest(allowAllKey.key), + "combo/slow-chat" + ); + assert.equal(allowAll.rejection, null); + + const denyAll = await policy.enforceApiKeyPolicy( + makePolicyRequest(denyAllKey.key), + "combo/fast-chat" + ); + assert.equal(denyAll.rejection.status, 403); + + const allowNamed = await policy.enforceApiKeyPolicy( + makePolicyRequest(allowNamedKey.key), + "combo/fast-chat" + ); + assert.equal(allowNamed.rejection, null); + + const denyOther = await policy.enforceApiKeyPolicy( + makePolicyRequest(allowNamedKey.key), + "combo/slow-chat" + ); + assert.equal(denyOther.rejection.status, 403); + + const directModel = await policy.enforceApiKeyPolicy( + makePolicyRequest(denyAllKey.key), + "openai/gpt-4.1" + ); + assert.equal(directModel.rejection, null); + + const routingRequest = makePolicyRequest(denyAllKey.key); + const routingCases = [ + { key: allowAllKey, model: "combo/slow-chat", status: null }, + { key: denyAllKey, model: "combo/fast-chat", status: 403 }, + { key: allowNamedKey, model: "combo/fast-chat", status: null }, + { key: allowNamedKey, model: "combo/slow-chat", status: 403 }, + { key: denyAllKey, model: "openai/gpt-4.1", status: null }, + ]; + for (const routingCase of routingCases) { + const metadata = await apiKeysDb.getApiKeyMetadata(routingCase.key.key); + assert.ok(metadata); + const rejection = await policy.validateApiKeyRoutingTarget( + routingRequest, + routingCase.key.key, + metadata, + routingCase.model + ); + assert.equal(rejection?.status ?? null, routingCase.status); + } +}); + test("enforceApiKeyPolicy applies configured throttle delay", async () => { const delayedKey = await createKeyWithPolicy({ throttleDelayMs: 25 }); const policy = await loadPolicy("throttle-delay"); diff --git a/tests/unit/api-manager-page-static.test.ts b/tests/unit/api-manager-page-static.test.ts index 005bf3c6e4..d211d23f25 100644 --- a/tests/unit/api-manager-page-static.test.ts +++ b/tests/unit/api-manager-page-static.test.ts @@ -90,6 +90,26 @@ test("permissions modal switch buttons declare button type", () => { } }); +test("permissions modal serializes All and empty Restrict Combo access distinctly", () => { + const source = readApiManagerPage(); + + assert.match( + source, + /import \{ ALL_COMBOS_ACCESS_RULE \} from "@\/shared\/constants\/comboAccess";/ + ); + assert.match( + source, + /const \[allowAllCombos, setAllowAllCombos\] = useState\(\s*apiKey\?\.allowedCombos\?\.includes\(ALL_COMBOS_ACCESS_RULE\) === true\s*\)/ + ); + assert.match(source, /allowAllCombos \? \[ALL_COMBOS_ACCESS_RULE\] : selectedCombos/); + assert.match( + source, + /Array\.isArray\(key\.allowedCombos\) &&\s*!key\.allowedCombos\.includes\(ALL_COMBOS_ACCESS_RULE\)/ + ); + assert.match(source, /setAllowAllCombos\(false\)/); + assert.doesNotMatch(source, /!allowAllCombos && selectedCombos\.length === 0[^\n]*return/); +}); + test("permissions modal persists the per-key prompt-compression switch", () => { const source = readApiManagerPage(); const component = fs.readFileSync( diff --git a/tests/unit/check-migration-numbering.test.ts b/tests/unit/check-migration-numbering.test.ts index 6941fc1308..17d1416e86 100644 --- a/tests/unit/check-migration-numbering.test.ts +++ b/tests/unit/check-migration-numbering.test.ts @@ -109,7 +109,8 @@ test("frozen allowlists match the documented legacy and stacked-series gaps", () // 147 left the gap list when 147_api_keys_model_access_mode.sql landed (same pattern as 143). assert.equal((KNOWN_GAPS as Set).has("147"), false); assert.ok((KNOWN_GAPS as Set).has("148")); - assert.ok((KNOWN_GAPS as Set).has("149")); + // 149 left the gap list when 149_api_key_combo_access.sql landed (#10066). + assert.equal((KNOWN_GAPS as Set).has("149"), false); // "041" was removed from KNOWN_DUPLICATE_VERSIONS in 6A.3 (stale: no physical // duplicate for that prefix on disk anymore — only 041_compression_receipts.sql exists). assert.equal((KNOWN_DUPLICATE_VERSIONS as Set).has("041"), false); diff --git a/tests/unit/migration-149-api-key-combo-access.test.ts b/tests/unit/migration-149-api-key-combo-access.test.ts new file mode 100644 index 0000000000..9acdb75b45 --- /dev/null +++ b/tests/unit/migration-149-api-key-combo-access.test.ts @@ -0,0 +1,46 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import DatabaseSync from "better-sqlite3"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const migrationPath = path.join(repoRoot, "src/lib/db/migrations/149_api_key_combo_access.sql"); + +test("combo-access migration preserves legacy allow-all rows and named allowlists", () => { + const sql = fs.readFileSync(migrationPath, "utf8"); + const db = new DatabaseSync(":memory:"); + db.exec(` + CREATE TABLE api_keys ( + id TEXT PRIMARY KEY, + allowed_combos TEXT + ); + + INSERT INTO api_keys (id, allowed_combos) VALUES + ('legacy-null', NULL), + ('legacy-empty', '[]'), + ('legacy-blank', ''), + ('legacy-malformed', 'not-json'), + ('named', '["fast-chat"]'), + ('all', '["combo/*"]'); + `); + + db.exec(sql); + db.exec(sql); + + const rows = db.prepare("SELECT id, allowed_combos FROM api_keys ORDER BY id").all() as Array<{ + id: string; + allowed_combos: string; + }>; + const combosById = new Map( + rows.map((row) => [row.id, JSON.parse(row.allowed_combos) as string[]]) + ); + + assert.deepEqual(combosById.get("legacy-null"), ["combo/*"]); + assert.deepEqual(combosById.get("legacy-empty"), ["combo/*"]); + assert.deepEqual(combosById.get("legacy-blank"), ["combo/*"]); + assert.deepEqual(combosById.get("legacy-malformed"), ["combo/*"]); + assert.deepEqual(combosById.get("named"), ["fast-chat"]); + assert.deepEqual(combosById.get("all"), ["combo/*"]); +}); From faeca3bbac691c51dfc71293cb74dd7ed5f3f210 Mon Sep 17 00:00:00 2001 From: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:01:09 +0800 Subject: [PATCH 08/22] fix(providers): scope model target formats to providers (#10072) Co-authored-by: xz-dev Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: adevwithpurpose --- open-sse/config/providerModels.ts | 26 +++++++++-------------- tests/unit/chatcore-target-format.test.ts | 12 +++++++++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/open-sse/config/providerModels.ts b/open-sse/config/providerModels.ts index afffd4429b..ee0028116f 100644 --- a/open-sse/config/providerModels.ts +++ b/open-sse/config/providerModels.ts @@ -173,14 +173,11 @@ export function getModelTargetFormat(aliasOrId: string, modelId: string): string // Accept either the public alias ("cmd") or the raw provider id ("command-code"), // mirroring getProviderModels (same pattern as #2798/#3870). const alias = PROVIDER_ID_TO_ALIAS[aliasOrId] || aliasOrId; - const models = PROVIDER_MODELS[alias]; // Strip provider prefix if present: "openai/gpt-5.6-luna" → "gpt-5.6-luna" - const prefix = alias + "/"; - const bareModelId = - typeof modelId === "string" && modelId.startsWith(prefix) - ? modelId.slice(prefix.length) - : modelId; - const found = models?.find((m) => m.id === bareModelId); + const prefixes = [`${aliasOrId}/`, `${alias}/`]; + const prefix = prefixes.find((value) => modelId.startsWith(value)); + const bareModelId = prefix ? modelId.slice(prefix.length) : modelId; + const found = PROVIDER_MODELS[alias]?.find((m) => m.id === bareModelId); if (found?.targetFormat) return found.targetFormat; // #5842: OpenAI "*-pro" reasoning models (o1-pro, gpt-5.x-pro) are only served by // the native /v1/responses endpoint — /v1/chat/completions 404s ("only supported @@ -188,16 +185,13 @@ export function getModelTargetFormat(aliasOrId: string, modelId: string): string // covers dynamically-synced ids that post-date the catalog (same spirit as the gh // executor's /codex/i routing, 9router#102). Scoped to the openai alias so other // providers shipping *-pro ids keep their own endpoint semantics. - if (alias === "openai" && /-pro$/i.test(modelId)) return "openai-responses"; + if (alias === "openai" && /-pro$/i.test(bareModelId)) return "openai-responses"; // Model-level targetFormat is provider-scoped: a catalog entry declares how THIS - // provider's endpoint serves the model. When the provider has its own catalog but - // the model is not in it, do NOT import the global entry's tag — it encodes the - // DECLARING provider's endpoint semantics (e.g. ghe-copilot tags gpt-5.6-* as - // openai-responses, which must not hijack command-code's chat-shaped - // /alpha/generate → 502 "Invalid prompt: messages must not be empty"). Providers - // with no catalog at all keep the global fallback as their only metadata source. - if (models) return null; - return getGlobalModel(bareModelId)?.targetFormat ?? null; + // provider's endpoint serves the model — do NOT import another provider's tag. + // #9994 scoped this for providers WITH a catalog; #10072 extends it to catalogless + // providers (openai-compatible-chat-*), which previously inherited the declaring + // provider's endpoint semantics via the global fallback. + return null; } export function getModelStripTypes(aliasOrId: string, modelId: string): string[] { const models = PROVIDER_MODELS[aliasOrId]; diff --git a/tests/unit/chatcore-target-format.test.ts b/tests/unit/chatcore-target-format.test.ts index b820b245fe..480ae01ca6 100644 --- a/tests/unit/chatcore-target-format.test.ts +++ b/tests/unit/chatcore-target-format.test.ts @@ -58,6 +58,18 @@ test("delegates byte-identically for a normal model (no apiFormat / no custom ov assert.deepEqual(r, expected("openai", "gpt-4o", undefined, undefined, undefined)); }); +test("provider-local target format does not leak from another provider", () => { + const r = resolveChatCoreTargetFormat({ + provider: "openai-compatible-chat-example", + resolvedModel: "gpt-5.6-sol", + apiFormat: undefined, + sourceFormat: FORMATS.OPENAI_RESPONSES, + customModelTargetFormat: undefined, + providerSpecificData: undefined, + }); + assert.equal(r.targetFormat, FORMATS.OPENAI); +}); + test("customModelTargetFormat is used when the model has no registry target format", () => { const customModel = "totally-unknown-custom-model-xyz"; // precondition: the registry has no target format for this unknown model From 8ff3a1dda3dcb0869cee6c6e5b934fc25a99d5c6 Mon Sep 17 00:00:00 2001 From: Sahil Singh Date: Mon, 17 Aug 2026 15:31:46 +0530 Subject: [PATCH 09/22] fix(mcp): dynamically generate web search provider enum from registry (#10209) * fix(mcp): dynamically generate web search provider enum from registry * test(mcp): add contract test for dynamic web search provider enum Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(mcp): restore search.ts eslint suppression, type builder maps, fix firecrawl searchType arg The enum-dynamic refactor dropped search.ts's no-explicit-any suppression while a new Record map re-introduced anys, and the response normalizer map swapped the firecrawl searchType argument with query. Type both maps explicitly, restore the base suppression (33 pre-existing anys), and pass searchType (not query) to normalizeFirecrawlSearchResponse. Co-authored-by: diegosouzapw --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: adevwithpurpose Co-authored-by: sadSanta-07 Co-authored-by: diegosouzapw --- config/quality/eslint-suppressions.json | 2 +- open-sse/config/searchRegistry.ts | 1 + open-sse/handlers/search.ts | 99 +++++++++++++------ open-sse/mcp-server/schemas/providerEnums.ts | 17 ++++ open-sse/mcp-server/schemas/tools.ts | 13 +-- open-sse/mcp-server/server.ts | 11 +-- ...-web-search-provider-enum-contract.test.ts | 74 ++++++++++++++ 7 files changed, 165 insertions(+), 52 deletions(-) create mode 100644 open-sse/mcp-server/schemas/providerEnums.ts create mode 100644 tests/unit/mcp-web-search-provider-enum-contract.test.ts diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 6e8eaf1238..25a55d63d0 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -3319,4 +3319,4 @@ "count": 5 } } -} \ No newline at end of file +} diff --git a/open-sse/config/searchRegistry.ts b/open-sse/config/searchRegistry.ts index b742ad4914..8baf51deff 100644 --- a/open-sse/config/searchRegistry.ts +++ b/open-sse/config/searchRegistry.ts @@ -30,6 +30,7 @@ export interface SearchProviderConfig { * credentialed provider is available, or when requested explicitly by id. */ fallbackOnly?: boolean; + disabled?: boolean; } export const SEARCH_PROVIDERS: Record = { diff --git a/open-sse/handlers/search.ts b/open-sse/handlers/search.ts index 366ae0014d..5f11d34c53 100644 --- a/open-sse/handlers/search.ts +++ b/open-sse/handlers/search.ts @@ -20,6 +20,7 @@ import { randomUUID } from "crypto"; import { getSearchProvider, type SearchProviderConfig } from "../config/searchRegistry.ts"; import { buildPerplexityRequest, parsePerplexitySearchOptions } from "./search/perplexitySearch.ts"; import * as fcSearch from "./search/firecrawlSearch.ts"; +import { type FirecrawlSearchEnvelope } from "./search/firecrawlSearch.ts"; import { freeWebSearch } from "../services/freeWebSearch.ts"; import { saveCallLog } from "@/lib/usageDb"; import { safeOutboundFetch } from "@/shared/network/safeOutboundFetch"; @@ -304,7 +305,10 @@ function buildSerperRequest( url: `${config.baseUrl}${endpoint}`, init: { method: "POST", - headers: { "Content-Type": "application/json", ...(params.token ? { "X-API-Key": params.token } : {}) }, + headers: { + "Content-Type": "application/json", + ...(params.token ? { "X-API-Key": params.token } : {}), + }, body: JSON.stringify(body), }, }; @@ -322,7 +326,10 @@ function buildBraveRequest( url: `${config.baseUrl}${endpoint}?${qp}`, init: { method: "GET", - headers: { Accept: "application/json", ...(params.token ? { "X-Subscription-Token": params.token } : {}) }, + headers: { + Accept: "application/json", + ...(params.token ? { "X-Subscription-Token": params.token } : {}), + }, }, }; } @@ -348,7 +355,10 @@ function buildExaRequest( url: config.baseUrl, init: { method: "POST", - headers: { "Content-Type": "application/json", ...(params.token ? { "x-api-key": params.token } : {}) }, + headers: { + "Content-Type": "application/json", + ...(params.token ? { "x-api-key": params.token } : {}), + }, body: JSON.stringify(body), }, }; @@ -597,22 +607,33 @@ function buildOllamaRequest( }; } +type SearchRequestBuilder = ( + config: SearchProviderConfig, + params: SearchRequestParams +) => { url: string; init: RequestInit }; + +const requestBuilders: Record = { + "serper-search": buildSerperRequest, + "brave-search": buildBraveRequest, + "perplexity-search": buildPerplexityRequest, + "exa-search": buildExaRequest, + "tavily-search": buildTavilyRequest, + firecrawl: fcSearch.buildFirecrawlSearchRequest, + "google-pse-search": buildGooglePseRequest, + "linkup-search": buildLinkupRequest, + "searchapi-search": buildSearchApiRequest, + "youcom-search": buildYouComRequest, + "searxng-search": buildSearxngRequest, + "ollama-search": buildOllamaRequest, +}; + function buildRequest( config: SearchProviderConfig, params: SearchRequestParams ): { url: string; init: RequestInit } { - if (config.id === "serper-search") return buildSerperRequest(config, params); - if (config.id === "brave-search") return buildBraveRequest(config, params); - if (config.id === "perplexity-search") return buildPerplexityRequest(config, params); - if (config.id === "exa-search") return buildExaRequest(config, params); - if (config.id === "tavily-search") return buildTavilyRequest(config, params); - if (config.id === "firecrawl") return fcSearch.buildFirecrawlSearchRequest(config, params); - if (config.id === "google-pse-search") return buildGooglePseRequest(config, params); - if (config.id === "linkup-search") return buildLinkupRequest(config, params); - if (config.id === "searchapi-search") return buildSearchApiRequest(config, params); - if (config.id === "youcom-search") return buildYouComRequest(config, params); - if (config.id === "searxng-search") return buildSearxngRequest(config, params); - if (config.id === "ollama-search") return buildOllamaRequest(config, params); + const builder = requestBuilders[config.id]; + if (builder) return builder(config, params); + // Fallback for future providers: POST with bearer auth return { url: resolveSearchBaseUrl(config, params), @@ -1161,29 +1182,40 @@ async function tryZaiMCPProvider( } } +type SearchResponseNormalizer = ( + data: unknown, + query: string, + searchType: string +) => { results: SearchResult[]; totalResults: number | null }; + +const responseNormalizers: Record = { + "serper-search": normalizeSerperResponse, + "brave-search": normalizeBraveResponse, + "perplexity-search": normalizePerplexityResponse, + "exa-search": normalizeExaResponse, + "tavily-search": normalizeTavilyResponse, + firecrawl: (data: FirecrawlSearchEnvelope, _query: string, searchType: string) => + fcSearch.normalizeFirecrawlSearchResponse(data, searchType, makeResult), + "google-pse-search": normalizeGooglePseResponse, + "linkup-search": normalizeLinkupResponse, + "searchapi-search": normalizeSearchApiResponse, + "youcom-search": normalizeYouComResponse, + "searxng-search": normalizeSearxngResponse, + "ollama-search": normalizeOllamaResponse, +}; + function normalizeResponse( providerId: string, data: any, query: string, searchType: string ): { results: SearchResult[]; totalResults: number | null } { - if (providerId === "serper-search") return normalizeSerperResponse(data, query, searchType); - if (providerId === "brave-search") return normalizeBraveResponse(data, query, searchType); - if (providerId === "perplexity-search") - return normalizePerplexityResponse(data, query, searchType); - if (providerId === "exa-search") return normalizeExaResponse(data, query, searchType); - if (providerId === "tavily-search") return normalizeTavilyResponse(data, query, searchType); - if (providerId === "firecrawl") - return fcSearch.normalizeFirecrawlSearchResponse(data, searchType, makeResult); - if (providerId === "google-pse-search") - return normalizeGooglePseResponse(data, query, searchType); - if (providerId === "linkup-search") return normalizeLinkupResponse(data, query, searchType); - if (providerId === "searchapi-search") return normalizeSearchApiResponse(data, query, searchType); - if (providerId === "youcom-search") return normalizeYouComResponse(data, query, searchType); - if (providerId === "searxng-search") return normalizeSearxngResponse(data, query, searchType); - if (providerId === "ollama-search") return normalizeOllamaResponse(data, query, searchType); + const normalizer = responseNormalizers[providerId]; + if (normalizer) return normalizer(data, query, searchType); + return { results: [], totalResults: null }; } + export async function handleSearch(options: SearchHandlerOptions): Promise { const { query, @@ -1221,6 +1253,13 @@ export async function handleSearch(options: SearchHandlerOptions): Promise !provider.disabled) + .map((provider) => provider.id); + + if (activeProviders.length === 0) { + return ["none_available"]; + } + + return activeProviders as [string, ...string[]]; +} diff --git a/open-sse/mcp-server/schemas/tools.ts b/open-sse/mcp-server/schemas/tools.ts index 88d427e041..d0d1c634cc 100644 --- a/open-sse/mcp-server/schemas/tools.ts +++ b/open-sse/mcp-server/schemas/tools.ts @@ -12,6 +12,7 @@ import { z } from "zod"; import { toolSearchTool } from "./toolSearch.ts"; import { pickFastestModelTool } from "./pickFastestModel.ts"; +import { getActiveSearchProviders } from "./providerEnums"; import { CCR_MCP_TOOLS } from "./ccrTools.ts"; import { radarCatalogTool } from "./radarCatalog.ts"; import { @@ -455,17 +456,7 @@ export const webSearchInput = z.object({ .describe("Maximum number of search results to return"), search_type: z.enum(["web", "news"]).default("web").describe("Type of search to perform"), provider: z - .enum([ - "serper-search", - "brave-search", - "perplexity-search", - "exa-search", - "tavily-search", - "google-pse-search", - "linkup-search", - "searchapi-search", - "searxng-search", - ]) + .enum(getActiveSearchProviders()) .optional() .describe("Specific search provider to use"), }); diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index 7f346ffd3e..e5c65f8c62 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -598,16 +598,7 @@ async function handleWebSearch(args: { query: string; max_results?: number; search_type?: "web" | "news"; - provider?: - | "serper-search" - | "brave-search" - | "perplexity-search" - | "exa-search" - | "tavily-search" - | "google-pse-search" - | "linkup-search" - | "searchapi-search" - | "searxng-search"; + provider?: string; }) { const start = Date.now(); try { diff --git a/tests/unit/mcp-web-search-provider-enum-contract.test.ts b/tests/unit/mcp-web-search-provider-enum-contract.test.ts new file mode 100644 index 0000000000..e8a48e5329 --- /dev/null +++ b/tests/unit/mcp-web-search-provider-enum-contract.test.ts @@ -0,0 +1,74 @@ +// #10209 — contract test: the MCP `omniroute_web_search` `provider` enum is now +// generated dynamically from the search registry (`getActiveSearchProviders`). +// This pins the invariant that the dynamic enum does not silently break the Zod +// schema exposed to MCP clients (or the tool's scope), regardless of future +// registry additions/removals or `disabled` flags. If the registry drifts, this +// test turns red before any client sees a broken/widened contract. +import test from "node:test"; +import assert from "node:assert/strict"; + +const { getActiveSearchProviders } = await import("../../open-sse/mcp-server/schemas/providerEnums.ts"); +const { SEARCH_PROVIDERS } = await import("../../open-sse/config/searchRegistry.ts"); +const { webSearchInput, webSearchTool } = await import("../../open-sse/mcp-server/schemas/tools.ts"); + +// The provider set the tool historically exposed to MCP clients (the hardcoded +// enum this PR replaced). Every one of these MUST keep parsing so existing +// clients are never broken by the dynamic enum. +const LEGACY_CONTRACT_PROVIDERS = [ + "serper-search", + "brave-search", + "perplexity-search", + "exa-search", + "tavily-search", + "google-pse-search", + "linkup-search", + "searchapi-search", + "searxng-search", +] as const; + +function activeRegistryIds(): string[] { + return Object.values(SEARCH_PROVIDERS) + .filter((p) => !p.disabled) + .map((p) => p.id) + .sort(); +} + +test("getActiveSearchProviders() is a non-empty tuple of active registry providers", () => { + const active = getActiveSearchProviders(); + assert.ok(Array.isArray(active), "should return an array (Zod enum tuple)"); + assert.ok(active.length > 0, "dynamic enum must never be empty"); + assert.deepEqual([...active].sort(), activeRegistryIds()); +}); + +test("web_search inputSchema provider enum equals the active provider set", () => { + const shape = webSearchInput.shape as { provider: { unwrap: () => unknown } }; + const providerManager = shape.provider; // ZodOptional around the ZodEnum + const unwrapped = providerManager.unwrap() as { options: string[] }; + const options = unwrapped.options; + assert.ok(Array.isArray(options), "provider field should be an enum"); + assert.ok(options.length > 0, "provider enum should have at least one value"); + assert.deepEqual([...options].sort(), activeRegistryIds()); +}); + +test("dynamic enum is a superset of the legacy contract (existing clients not broken)", () => { + for (const id of LEGACY_CONTRACT_PROVIDERS) { + const result = webSearchInput.safeParse({ query: "test", provider: id }); + assert.ok(result.success, `legacy provider "${id}" must still parse`); + } + // Every active registry provider must also be selectable by an MCP client. + for (const id of activeRegistryIds()) { + const result = webSearchInput.safeParse({ query: "test", provider: id }); + assert.ok(result.success, `active registry provider "${id}" must parse`); + } +}); + +test("unknown provider values are rejected (contract stays tight)", () => { + const result = webSearchInput.safeParse({ query: "test", provider: "no-such-search-provider" }); + assert.equal(result.success, false); +}); + +test("tool registration + scope are unaffected by the dynamic enum", () => { + assert.equal(webSearchTool.name, "omniroute_web_search"); + assert.equal(webSearchTool.inputSchema, webSearchInput); + assert.ok(webSearchTool.scopes.includes("execute:search"), "web search tool scope must be retained"); +}); \ No newline at end of file From 3e8a8f71cc089ead1adca157d2249adbfd1ce31e Mon Sep 17 00:00:00 2001 From: Benson K B Date: Mon, 17 Aug 2026 15:32:48 +0530 Subject: [PATCH 10/22] fix(providers): add PATCH handler to provider connection route (CLI rotate 405) (#10366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(providers): add PATCH handler to provider connection route The OpenAPI spec and the CLI (omniroute providers rotate, generated api-commands) both use PATCH /api/providers/[id], but the route only implemented PUT — PATCH requests returned 405 and key rotation via the CLI silently failed while reporting success (the DB-write fallback only catches thrown exceptions, not non-OK HTTP responses). Add a PATCH handler delegating to the PUT handler: both apply the same partial-update schema, so the semantics are identical. Regression test proves the PATCH export exists and delegates into the shared auth path; verified to fail without the fix. * docs(changelog): note PATCH provider route fix (PR #10366) * fix(providers): make PATCH delegation test environment-robust The 'PATCH delegates to PUT' assertion hardcoded a 401, which only holds when management auth is enforced (dev). In the CI unit-test env auth is not required, so the flow falls through to 'Connection not found' (404) for an unknown id — the test failed on the status code while the PATCH->PUT delegation itself is correct. Assert on delegation equivalence instead: PATCH must never 405 (the regression) and must return the same status as PUT for the same input. Co-authored-by: diegosouzapw * test(providers): use fresh Request per handler in PATCH delegation test The same Request was passed to both PATCH and PUT — PUT consumes the body via request.json(), so the second call got an empty body (400 validation) vs the first (404 not-found): a false status mismatch on bases where management auth is bypassed in the test env (release v3.8.50). Fresh Request per invocation makes identical inputs produce identical statuses. --------- Co-authored-by: benzntech Co-authored-by: diegosouzapw --- CHANGELOG.md | 1 + src/app/api/providers/[id]/route.ts | 9 +++ .../unit/providers-route-patch-method.test.ts | 66 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 tests/unit/providers-route-patch-method.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ff47e9757a..49668a6214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,7 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e ### 🐛 Bug Fixes +- **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366) - **executors**: fix internal timeout misclassified as client disconnect (499) for 7 niche executors — pass TimeoutError reason to controller.abort() (#8197 side-finding) - test(combo): guard auto/best-free never leaks the combo name as a model (#7754) - fix(vision-bridge): describe-model no longer returns unreachable "openai/gpt-4o-mini" when every vision-capable provider is unreachable on the instance — returns null instead and surfaces a clear error (#8430) diff --git a/src/app/api/providers/[id]/route.ts b/src/app/api/providers/[id]/route.ts index adb5840fa0..562dad0744 100644 --- a/src/app/api/providers/[id]/route.ts +++ b/src/app/api/providers/[id]/route.ts @@ -376,6 +376,15 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id: } } +// PATCH /api/providers/[id] - Update connection (partial) +// The OpenAPI spec and the CLI (`omniroute providers rotate`, generated +// api-commands) both use PATCH, but only PUT was implemented — PATCH requests +// 405'd. PATCH and PUT share the same update semantics here (the schema only +// applies provided fields), so delegate to the PUT handler. +export async function PATCH(request: Request, ctx: { params: Promise<{ id: string }> }) { + return PUT(request, ctx); +} + // DELETE /api/providers/[id] - Delete connection export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { const authError = await requireManagementAuth(request); diff --git a/tests/unit/providers-route-patch-method.test.ts b/tests/unit/providers-route-patch-method.test.ts new file mode 100644 index 0000000000..23b3dcd5af --- /dev/null +++ b/tests/unit/providers-route-patch-method.test.ts @@ -0,0 +1,66 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// Regression test for the providers-route PATCH gap: the OpenAPI spec and the +// CLI (`omniroute providers rotate`, generated api-commands) both use +// PATCH /api/providers/[id], but the route only implemented PUT — PATCH +// requests 405'd and `providers rotate --new-key` silently failed while +// reporting success. See PR fix: the route now exports a PATCH handler that +// delegates to PUT (both apply the same partial-update schema). + +async function loadRoute() { + return await import(new URL("../../src/app/api/providers/[id]/route.ts", import.meta.url)); +} + +test("providers [id] route exports a PATCH handler (CLI rotate 405 regression)", async () => { + const route = await loadRoute(); + assert.equal( + typeof route.PATCH, + "function", + "PATCH handler must exist — CLI rotate sends PATCH per the OpenAPI spec" + ); +}); + +test("PATCH handler delegates to PUT (same partial-update semantics)", async () => { + const route = await loadRoute(); + // The PATCH export delegates to PUT; both share the same update logic and + // are distinct function references (wrapper). A fixed status expectation is + // environment-dependent: management auth is enforced on dev (PUT returns 401 + // without a credential) but NOT in the CI unit-test env, where the flow + // falls through to "Connection not found" (404) for an unknown id. So assert + // on delegation equivalence instead: PATCH must never 405 (the regression) + // and must return the exact same status as PUT for the same input. + const ctx = { params: Promise.resolve({ id: "test-id" }) }; + // Fresh Request per invocation: PUT reads the body via request.json(), + // which consumes the body stream — reusing one Request for both calls would + // give the second call an empty body (400 validation) vs the first (404 + // not-found), a false mismatch. Identical inputs must produce identical + // statuses. + const patchRequest = new Request("http://localhost/api/providers/test-id", { + method: "PATCH", + body: JSON.stringify({ name: "x" }), + }); + const putRequest = new Request("http://localhost/api/providers/test-id", { + method: "PUT", + body: JSON.stringify({ name: "x" }), + }); + const patchResult = await route.PATCH(patchRequest, ctx); + const putResult = await route.PUT(putRequest, ctx); + assert.ok(patchResult, "PATCH should return a response, not 405"); + assert.notEqual( + patchResult.status, + 405, + "PATCH must be routed — before the fix Next.js returned 405 Method Not Allowed" + ); + assert.equal( + patchResult.status, + putResult.status, + "PATCH must delegate to PUT's handler (identical status for the same input)" + ); +}); + +test("providers [id] route still exports PUT and DELETE handlers", async () => { + const route = await loadRoute(); + assert.equal(typeof route.PUT, "function"); + assert.equal(typeof route.DELETE, "function"); +}); From 8bd0b840f6ae3cd84c16eb42801fe2d30d90859a Mon Sep 17 00:00:00 2001 From: Chewji <126886556+Chewji9875@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:04:07 +0700 Subject: [PATCH 11/22] fix(antigravity): unblock Gemini and Claude reasoning capabilities (#10376) * fix(antigravity): unblock Gemini and Claude reasoning capabilities * fix(antigravity): align two unit tests with unblocked Gemini/Claude reasoning The PR unblocks Antigravity Gemini/Claude reasoning (removed from REASONING_UNSUPPORTED_PATTERNS, mirroring model-capabilities-registry.test.ts). models-catalog-combo-metadata and services-branch-hardening still asserted the pre-PR deny contract; align them to the new verified behavior. No production code changed. Co-authored-by: diegosouzapw --------- Co-authored-by: adevwithpurpose Co-authored-by: Chewji9875 Co-authored-by: diegosouzapw --- src/lib/modelCapabilities.ts | 7 - ...avity-thinking-config-preservation.test.ts | 130 ++++++++++++++++++ .../unit/model-capabilities-registry.test.ts | 4 +- .../models-catalog-combo-metadata.test.ts | 16 +-- tests/unit/services-branch-hardening.test.ts | 4 +- 5 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 tests/unit/antigravity-thinking-config-preservation.test.ts diff --git a/src/lib/modelCapabilities.ts b/src/lib/modelCapabilities.ts index 3c2e037fe2..83afd6a34b 100644 --- a/src/lib/modelCapabilities.ts +++ b/src/lib/modelCapabilities.ts @@ -47,13 +47,6 @@ const TOOL_CALLING_UNSUPPORTED_PATTERNS: string[] = [ "stable-diffusion", ]; const REASONING_UNSUPPORTED_PATTERNS = [ - "antigravity/claude-sonnet-4-6", - "antigravity/claude-sonnet-4-5", - "antigravity/claude-sonnet-4", - // Non-Claude antigravity models don't support thinking params (#1361) - "antigravity/gemini-", - "antigravity/gpt-oss-", - "antigravity/gemini-3", "antigravity/tab_", // Specialty / non-chat surfaces (#8016) "whisper", diff --git a/tests/unit/antigravity-thinking-config-preservation.test.ts b/tests/unit/antigravity-thinking-config-preservation.test.ts new file mode 100644 index 0000000000..904d453287 --- /dev/null +++ b/tests/unit/antigravity-thinking-config-preservation.test.ts @@ -0,0 +1,130 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + getResolvedModelCapabilities, + supportsReasoning, +} from "../../src/lib/modelCapabilities.ts"; +import { applyThinkingBudget } from "../../open-sse/services/thinkingBudget.ts"; +import { translateRequest } from "../../open-sse/translator/index.ts"; +import { FORMATS } from "../../open-sse/translator/formats.ts"; + +test("antigravity reasoning capabilities: Gemini and Claude models support reasoning", () => { + const geminiModels = [ + "antigravity/gemini-3-flash-agent", + "antigravity/gemini-pro-agent", + "antigravity/gemini-3.1-pro-low", + "antigravity/claude-sonnet-4-6", + "antigravity/claude-opus-4-6-thinking", + ]; + + for (const modelId of geminiModels) { + const isReasoning = supportsReasoning(modelId); + assert.equal(isReasoning, true, `supportsReasoning should be true for ${modelId}`); + + const caps = getResolvedModelCapabilities(modelId); + assert.equal(caps.reasoning, true, `caps.reasoning should be true for ${modelId}`); + assert.equal( + caps.supportsThinking, + true, + `caps.supportsThinking should be true for ${modelId}` + ); + } + + // Passthrough / unlisted Gemini model should still heuristically resolve reasoning + assert.equal(supportsReasoning("antigravity/gemini-2.5-pro"), true); + const gemini25Caps = getResolvedModelCapabilities("antigravity/gemini-2.5-pro"); + assert.equal(gemini25Caps.reasoning, true); + + // Non-reasoning models like tab completion should return false + assert.equal(supportsReasoning("antigravity/tab_flash_lite"), false); + const tabCaps = getResolvedModelCapabilities("antigravity/tab_flash_lite"); + assert.equal(tabCaps.reasoning, false); +}); + +test("antigravity request pipeline: applyThinkingBudget preserves reasoning params", () => { + const req: Record = { + model: "antigravity/gemini-pro-agent", + messages: [{ role: "user", content: "Solve math problem" }], + reasoning_effort: "max", + }; + + const processed = applyThinkingBudget(req); + assert.equal( + (processed as Record).reasoning_effort, + "max", + "reasoning_effort must not be stripped" + ); +}); + +test("antigravity translator: translates reasoning_effort into Gemini thinkingConfig", () => { + const inputReq = { + model: "antigravity/gemini-pro-agent", + messages: [{ role: "user", content: "Solve math problem" }], + reasoning_effort: "max", + }; + + const translated = translateRequest( + FORMATS.OPENAI, + FORMATS.ANTIGRAVITY, + "antigravity/gemini-pro-agent", + inputReq, + true, + null, + "antigravity" + ); + + const generationConfig = ( + translated as { request?: { generationConfig?: Record } } + )?.request?.generationConfig; + + assert.ok(generationConfig, "generationConfig must exist in Cloud Code envelope"); + assert.ok( + generationConfig.thinkingConfig, + "thinkingConfig must exist in generationConfig for Gemini reasoning models" + ); + assert.equal( + (generationConfig.thinkingConfig as { includeThoughts?: boolean }).includeThoughts, + true + ); + assert.equal( + typeof (generationConfig.thinkingConfig as { thinkingBudget?: number }).thinkingBudget, + "number" + ); + assert.ok( + (generationConfig.thinkingConfig as { thinkingBudget: number }).thinkingBudget > 0, + "thinkingBudget should be positive" + ); +}); + +test("antigravity translator: Claude models bump maxOutputTokens and strip raw thinkingConfig", () => { + const inputReq = { + model: "antigravity/claude-sonnet-4-6", + messages: [{ role: "user", content: "Explain quantum mechanics" }], + reasoning_effort: "high", + }; + + const translated = translateRequest( + FORMATS.OPENAI, + FORMATS.ANTIGRAVITY, + "antigravity/claude-sonnet-4-6", + inputReq, + true, + null, + "antigravity" + ); + + const generationConfig = ( + translated as { request?: { generationConfig?: Record } } + )?.request?.generationConfig; + + assert.ok(generationConfig, "generationConfig must exist in Cloud Code envelope"); + assert.equal( + generationConfig.thinkingConfig, + undefined, + "raw thinkingConfig must be stripped for Claude models on Antigravity" + ); + assert.ok( + (generationConfig.maxOutputTokens as number) >= 16384, + "maxOutputTokens should be preserved/bumped for Claude reasoning" + ); +}); diff --git a/tests/unit/model-capabilities-registry.test.ts b/tests/unit/model-capabilities-registry.test.ts index 93f80274e5..c23e05a061 100644 --- a/tests/unit/model-capabilities-registry.test.ts +++ b/tests/unit/model-capabilities-registry.test.ts @@ -165,7 +165,7 @@ test("Antigravity Gemini 3.5 upstream IDs share the Flash capability profile", ( const capabilities = modelCapabilities.getResolvedModelCapabilities(`antigravity/${modelId}`); assert.equal(capabilities.contextWindow, 1048576, modelId); assert.equal(capabilities.maxOutputTokens, 65536, modelId); - assert.equal(capabilities.supportsThinking, false, modelId); + assert.equal(capabilities.supportsThinking, true, modelId); assert.equal(capabilities.supportsTools, true, modelId); assert.equal(capabilities.supportsVision, true, modelId); } @@ -184,7 +184,7 @@ test("Antigravity Gemini 3.7 and 3.6 tier IDs share the Flash capability profile const capabilities = modelCapabilities.getResolvedModelCapabilities(`antigravity/${modelId}`); assert.equal(capabilities.contextWindow, 1048576, modelId); assert.equal(capabilities.maxOutputTokens, 65536, modelId); - assert.equal(capabilities.supportsThinking, false, modelId); + assert.equal(capabilities.supportsThinking, true, modelId); assert.equal(capabilities.supportsTools, true, modelId); assert.equal(capabilities.supportsVision, true, modelId); } diff --git a/tests/unit/models-catalog-combo-metadata.test.ts b/tests/unit/models-catalog-combo-metadata.test.ts index 51d5c5360b..e262bf4971 100644 --- a/tests/unit/models-catalog-combo-metadata.test.ts +++ b/tests/unit/models-catalog-combo-metadata.test.ts @@ -87,18 +87,18 @@ test("single-target combo respects registry reasoning overrides before specs", a assert.equal(Object.hasOwn(capabilities, "effort_tiers"), false); }); -test("single-target combo respects resolved reasoning deny patterns", async () => { +test("single-target combo reflects unblocked Antigravity Gemini reasoning", async () => { await providersDb.createProviderConnection({ provider: "antigravity", authType: "oauth", - name: "antigravity-gemini-no-thinking-combo", + name: "antigravity-gemini-reasoning-combo", accessToken: "antigravity-test-token", isActive: true, testStatus: "active", providerSpecificData: {}, }); await combosDb.createCombo({ - name: "antigravity-gemini-no-thinking-combo", + name: "antigravity-gemini-reasoning-combo", strategy: "auto", models: ["antigravity/gemini-3.1-pro-high"], }); @@ -107,13 +107,13 @@ test("single-target combo respects resolved reasoning deny patterns", async () = new Request("http://localhost/api/v1/models") ); const body = (await response.json()) as { data: Array> }; - const combo = body.data.find((item) => item.id === "antigravity-gemini-no-thinking-combo"); + const combo = body.data.find((item) => item.id === "antigravity-gemini-reasoning-combo"); assert.equal(response.status, 200); assert.ok(combo); const capabilities = combo.capabilities as Record; - assert.equal(capabilities.reasoning, false); - assert.equal(capabilities.thinking, false); - assert.equal(capabilities.supportsThinking, false); - assert.equal(Object.hasOwn(capabilities, "effort_tiers"), false); + assert.equal(capabilities.reasoning, true); + assert.equal(capabilities.thinking, true); + assert.equal(capabilities.supportsThinking, true); + assert.equal(Object.hasOwn(capabilities, "effort_tiers"), true); }); diff --git a/tests/unit/services-branch-hardening.test.ts b/tests/unit/services-branch-hardening.test.ts index fa6458eab5..085cd4df53 100644 --- a/tests/unit/services-branch-hardening.test.ts +++ b/tests/unit/services-branch-hardening.test.ts @@ -113,8 +113,8 @@ test("model capability helpers cover denylist, empty input and default-safe path ); assert.equal(modelCapabilities.supportsReasoning(""), true); - assert.equal(modelCapabilities.supportsReasoning("antigravity/claude-sonnet-4-6"), false); - assert.equal(modelCapabilities.supportsReasoning("antigravity/claude-sonnet-4"), false); + assert.equal(modelCapabilities.supportsReasoning("antigravity/claude-sonnet-4-6"), true); + assert.equal(modelCapabilities.supportsReasoning("antigravity/claude-sonnet-4"), true); assert.equal(modelCapabilities.supportsReasoning("openai/nonexistent-default-safe-model"), true); }); From 48e5cf7fe41cad51922cba3259cb3990acc8becd Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:04:48 +0200 Subject: [PATCH 12/22] fix(sse): do not ZWJ-obfuscate the substring hermes in user text (#10488) Keep the #8350 Hermes system-prompt drops, but remove hermes from the factory obfuscate_words list so hostnames and CLI mentions stay intact. Co-authored-by: Ravi Tharuma --- .../fixes/10484-hermes-obfuscate-zwj.md | 1 + open-sse/services/systemTransforms.ts | 7 +++-- .../settings/components/RoutingTab.tsx | 2 -- .../unit/8350-hermes-oauth-usage-400.test.ts | 31 +++++++++++++++++++ tests/unit/system-transforms.test.ts | 2 -- 5 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fixes/10484-hermes-obfuscate-zwj.md diff --git a/changelog.d/fixes/10484-hermes-obfuscate-zwj.md b/changelog.d/fixes/10484-hermes-obfuscate-zwj.md new file mode 100644 index 0000000000..5e1dc60de1 --- /dev/null +++ b/changelog.d/fixes/10484-hermes-obfuscate-zwj.md @@ -0,0 +1 @@ +- fix(sse): stop ZWJ-obfuscating the substring "hermes" in user messages and hostnames (#10484) diff --git a/open-sse/services/systemTransforms.ts b/open-sse/services/systemTransforms.ts index e7542619e3..2bed25d741 100644 --- a/open-sse/services/systemTransforms.ts +++ b/open-sse/services/systemTransforms.ts @@ -96,9 +96,10 @@ export const DEFAULT_OBFUSCATE_WORDS = [ // Open WebUI additions "openwebui", "open-webui", - // Hermes additions (#8350) - "hermes-agent", - "hermes", + // Do not add "hermes" / "hermes-agent" here. #8350 is handled by + // HERMES_PARAGRAPH_ANCHORS + HERMES_IDENTITY_PREFIXES (system-prompt + // drops only). ZWJ on the short substring "hermes" rewrites user + // messages and hostnames (#10484). ]; /** diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index 22462ce866..5c844f80cb 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -81,8 +81,6 @@ const DEFAULT_OBFUSCATE_WORDS = [ "codecompanion", "openwebui", "open-webui", - "hermes-agent", - "hermes", ]; // Mirror of DEFAULT_SYSTEM_TRANSFORMS_CONFIG from open-sse/services/systemTransforms.ts. diff --git a/tests/unit/8350-hermes-oauth-usage-400.test.ts b/tests/unit/8350-hermes-oauth-usage-400.test.ts index fd502183c4..51288c23be 100644 --- a/tests/unit/8350-hermes-oauth-usage-400.test.ts +++ b/tests/unit/8350-hermes-oauth-usage-400.test.ts @@ -68,3 +68,34 @@ test("non-Hermes system prompt passes through byte-identical through the claude "a normal operator system prompt with no third-party-agent anchors must pass through untouched" ); }); + +// #10484 — #8358 added "hermes" to DEFAULT_OBFUSCATE_WORDS. The ZWJ op +// targets user messages with a case-insensitive, no-word-boundary regex, so +// hostnames and ordinary mentions of the OmniRoute hermes CLI tool were +// rewritten. System-prompt identity drops (#8350) must stay; user text must not +// be mutated. +test("user message containing hermes hostname stays byte-identical (#10484)", () => { + const body = { + system: [ + { + type: "text", + text: "You are a helpful operator-configured assistant. Follow company policy X and always answer in English.", + }, + ], + messages: [ + { + role: "user", + content: "1. hermes\n2. hermes.example.ts.net\n3. Hermes on agent-001\n4. hermeS", + }, + ], + }; + const before = JSON.stringify(body); + applySystemTransformPipeline(PROVIDER_CLAUDE, body, DEFAULT_SYSTEM_TRANSFORMS_CONFIG); + assert.equal( + JSON.stringify(body), + before, + "user text containing the substring hermes must not receive ZWJ obfuscation" + ); + const content = (body.messages[0] as { content: string }).content; + assert.equal(content.includes("\u200d"), false, "no zero-width joiner in user text"); +}); diff --git a/tests/unit/system-transforms.test.ts b/tests/unit/system-transforms.test.ts index cf1f7391e0..047063d663 100644 --- a/tests/unit/system-transforms.test.ts +++ b/tests/unit/system-transforms.test.ts @@ -502,8 +502,6 @@ const UI_DEFAULTS_SNAPSHOT = { "codecompanion", "openwebui", "open-webui", - "hermes-agent", - "hermes", ], targets: ["system", "messages", "tools"], }, From 31b02ff85f94849895cfbdbcdcabcb9433c65844 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 17 Aug 2026 07:05:23 -0300 Subject: [PATCH 13/22] fix(responses): keep stream-aware TextDecoder across SSE transform chunks (#10223) (#10495) Co-authored-by: adevwithpurpose --- ...10223-deepseek-responses-sse-cjk-deltas.md | 1 + open-sse/transformer/responsesTransformer.ts | 12 +- .../responses-transformer-cjk-split.test.ts | 111 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/10223-deepseek-responses-sse-cjk-deltas.md create mode 100644 tests/unit/responses-transformer-cjk-split.test.ts diff --git a/changelog.d/fixes/10223-deepseek-responses-sse-cjk-deltas.md b/changelog.d/fixes/10223-deepseek-responses-sse-cjk-deltas.md new file mode 100644 index 0000000000..8f3c19bb20 --- /dev/null +++ b/changelog.d/fixes/10223-deepseek-responses-sse-cjk-deltas.md @@ -0,0 +1 @@ +- **fix(responses):** repair corrupted SSE deltas for non-ASCII streams by keeping a single stream-aware `TextDecoder` (`{ stream: true }`) across `transform()` calls instead of recreating it per chunk and decoding without the `stream` flag. When a multi-byte UTF-8 character (CJK/emoji) was split across two TCP chunks — common in Chinese streaming text — the per-chunk decoder truncated it to `U+FFFD`, corrupting every delta while the rebuilt `*.done` snapshot stayed internally identical ([#10223](https://github.com/diegosouzapw/OmniRoute/issues/10223)) \ No newline at end of file diff --git a/open-sse/transformer/responsesTransformer.ts b/open-sse/transformer/responsesTransformer.ts index a9c0db949e..1ef35e4aa6 100644 --- a/open-sse/transformer/responsesTransformer.ts +++ b/open-sse/transformer/responsesTransformer.ts @@ -231,6 +231,11 @@ export function createResponsesApiTransformStream( }; const encoder = new TextEncoder(); + // #10223: a stream:false TextDecoder recreated per transform() chunk has no + // cross-call state, so a multi-byte UTF-8 character (CJK/emoji) split across + // two TCP chunks got truncated to U+FFFD, corrupting the deltas. A single + // persistent decoder with { stream: true } carries pending bytes between chunks. + const decoder = new TextDecoder(); const nextSeq = () => ++state.seq; // Normalize output_index to a non-negative integer (replaces fragile parseInt calls) @@ -577,7 +582,7 @@ export function createResponsesApiTransformStream( (state.keepaliveTimer as { unref?: () => void })?.unref?.(); }, transform(chunk, controller) { - const text = new TextDecoder().decode(chunk); + const text = decoder.decode(chunk, { stream: true }); logger?.logInput(text.trim()); state.buffer += text; @@ -887,6 +892,11 @@ export function createResponsesApiTransformStream( }, flush(controller) { + // #10223: stream-end flush — drain any bytes the persistent decoder is + // still holding. With { stream:true } complete multi-byte chars are + // emitted within transform(), so normally there is nothing left; this + // only releases a terminating truncated byte and frees the decoder. + state.buffer += decoder.decode(); // Clear keepalive timer if (state.keepaliveTimer) { clearInterval(state.keepaliveTimer); diff --git a/tests/unit/responses-transformer-cjk-split.test.ts b/tests/unit/responses-transformer-cjk-split.test.ts new file mode 100644 index 0000000000..59cf48266c --- /dev/null +++ b/tests/unit/responses-transformer-cjk-split.test.ts @@ -0,0 +1,111 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// Regression guard for #10223 — DeepSeek /v1/responses corrupted SSE deltas. +// +// ROOT CAUSE (open-sse/transformer/responsesTransformer.ts:580): the transform() +// handler created a brand-new `new TextDecoder()` on every chunk and decoded it +// WITHOUT `{ stream: true }`. A stream:false decoder has no cross-call state, so +// whenever a multi-byte UTF-8 character (CJK: 3 bytes, emoji: 4) is split across +// two TCP chunks — the normal case in Chinese streaming text (the reporter's +// scenario), the trailing partial bytes are replaced with U+FFFD and the deltas +// accumulate garbage. +// +// This test feeds a CJK text split at a byte boundary INSIDE a multi-byte +// character and asserts a round-trip against the source text — NOT the +// `join(deltas) === done` invariant, which cannot catch this bug because done is +// rebuilt from the same corrupted buffer as the deltas. + +const { createResponsesApiTransformStream } = await import( + "../../open-sse/transformer/responsesTransformer.ts" +); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function concatBytes(parts) { + const total = parts.reduce((sum, part) => sum + part.length, 0); + const out = new Uint8Array(total); + let offset = 0; + for (const part of parts) { + out.set(part, offset); + offset += part.length; + } + return out; +} + +function parseSseOutput(output) { + return output + .trim() + .split("\n\n") + .map((entry) => { + const lines = entry.split("\n"); + const eventLine = lines.find((line) => line.startsWith("event: ")); + const dataLine = lines.find((line) => line.startsWith("data: ")); + return { + event: eventLine ? eventLine.slice("event: ".length) : null, + data: dataLine ? dataLine.slice("data: ".length) : null, + }; + }) + .filter((e) => e.event !== null || e.data !== null); +} + +async function runRawBytes(byteChunks, options = {}) { + const stream = createResponsesApiTransformStream(null, 3000, options); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + + const raw = []; + const readerTask = (async () => { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + if (value) raw.push(value); + } + })(); + + for (const chunk of byteChunks) { + await writer.write(chunk); + } + await writer.close(); + await readerTask; + + return decoder.decode(concatBytes(raw)); +} + +test("responses transform preserves multi-byte UTF-8 text split across byte chunks (#10223)", async () => { + const source = "REASONIX_中文测试_DEEPSEEK_OK"; + + const frame = (data) => + encoder.encode(`data: ${JSON.stringify(data)}\n\n`); + + const deltaChunk = frame({ + choices: [{ index: 0, delta: { content: source } }], + }); + const finishChunk = frame({ + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + }); + const full = concatBytes([deltaChunk, finishChunk]); + + // Split mid-byte inside the first 3-byte CJK character "中". + const contentPrefix = encoder.encode( + 'data: {"choices":[{"index":0,"delta":{"content":"' + ).length; + const boundary = contentPrefix + encoder.encode("REASONIX_").length + 1; + const chunkA = full.slice(0, boundary); + const chunkB = full.slice(boundary); + + const output = await runRawBytes([chunkA, chunkB]); + + const events = parseSseOutput(output); + const deltas = events + .filter((e) => e.event === "response.output_text.delta") + .map((e) => JSON.parse(e.data).delta); + + const doneEvent = events.find((e) => e.event === "response.output_text.done"); + const doneText = JSON.parse(doneEvent.data).text; + + // Round-trip against the SOURCE text — the invariant the old test missed. + assert.equal(deltas.join(""), source, "joined deltas should round-trip to the source text"); + assert.equal(doneText, source, "done snapshot should round-trip to the source text"); +}); \ No newline at end of file From 8dec2ad472c5e287dceb5deecc436e7c937f26f6 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 17 Aug 2026 07:06:00 -0300 Subject: [PATCH 14/22] fix(resilience): mark embed connection terminal on hard upstream failure so dead accounts are not re-hit (#10347) (#10506) Co-authored-by: adevwithpurpose --- open-sse/handlers/embeddings.ts | 23 ++++ .../providers/[provider]/embeddings/route.ts | 9 +- src/lib/embeddings/service.ts | 7 +- tests/unit/10347-embed-402-cooldown.test.ts | 103 ++++++++++++++++++ 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/unit/10347-embed-402-cooldown.test.ts diff --git a/open-sse/handlers/embeddings.ts b/open-sse/handlers/embeddings.ts index 7846ec3d58..0945e3138a 100644 --- a/open-sse/handlers/embeddings.ts +++ b/open-sse/handlers/embeddings.ts @@ -35,6 +35,7 @@ import { prepareStructuredEmbeddingRequest, } from "./embeddingStructuredInput.ts"; import { MAX_EMBEDDING_INLINE_ITEM_BYTES } from "@/shared/validation/schemas/apiV1"; +import { markAccountUnavailable } from "../../src/sse/services/auth.ts"; interface ClientRawRequest { endpoint: string; @@ -389,6 +390,28 @@ export async function handleEmbedding({ connectionId, }).catch(() => {}); + // #10347 — persist a connection-level failure marker on a hard upstream failure so + // the dead account is not re-selected and re-hit on the next embed request (chat + // parity). markAccountUnavailable classifies the status via checkFallbackError: a + // payment-required 402 becomes the TERMINAL state credits_exhausted (the terminal + // marker excludes the account from selection until an operator resets it), benign + // 4xx are a no-op, and terminal statuses are never overwritten. honors per-connection + // disableCooling. The write must never break the error response path, so it is + // best-effort. + if (connectionId) { + try { + await markAccountUnavailable( + connectionId, + response.status, + errorText, + provider, + model + ); + } catch { + // swallow — the upstream error response takes priority + } + } + return { success: false, status: response.status, diff --git a/src/app/api/v1/providers/[provider]/embeddings/route.ts b/src/app/api/v1/providers/[provider]/embeddings/route.ts index 01dbe5bc84..bb8f242290 100644 --- a/src/app/api/v1/providers/[provider]/embeddings/route.ts +++ b/src/app/api/v1/providers/[provider]/embeddings/route.ts @@ -84,7 +84,14 @@ export async function POST(request, { params }) { ); } - const result = await handleEmbedding({ body, credentials, log }); + const result = await handleEmbedding({ + body, + credentials, + log, + // #10347 — thread the selected connection id so a hard upstream failure cools + // the account instead of re-hitting it on every request. + connectionId: (credentials as { connectionId?: string } | null)?.connectionId ?? null, + }); if (result.success) { await clearRecoveredProviderState(credentials); diff --git a/src/lib/embeddings/service.ts b/src/lib/embeddings/service.ts index 5845cb773f..a615958b18 100644 --- a/src/lib/embeddings/service.ts +++ b/src/lib/embeddings/service.ts @@ -302,7 +302,12 @@ export async function createEmbeddingResponse( clientRawRequest: options.clientRawRequest || null, apiKeyId: options.apiKeyId || null, apiKeyName: options.apiKeyName || null, - connectionId: options.connectionId || null, + // #10347 — thread the selected connection id so handleEmbedding can cool the + // account on a hard upstream failure (previously always null on /v1/embeddings). + connectionId: + ((credentials as { connectionId?: string } | null)?.connectionId) || + options.connectionId || + null, }); const result = connectionIdForProxy diff --git a/tests/unit/10347-embed-402-cooldown.test.ts b/tests/unit/10347-embed-402-cooldown.test.ts new file mode 100644 index 0000000000..ddb98259ba --- /dev/null +++ b/tests/unit/10347-embed-402-cooldown.test.ts @@ -0,0 +1,103 @@ +/** + * TDD regression (#10347): the embed path reads a connection's cooldown at + * selection time but NEVER writes one on a terminal upstream failure. A Mistral + * (or any) connection returning HTTP 402 "payment required — Check your + * subscription" on embeds is re-selected and re-hit upstream on every request — + * the repeated EMBED/ERROR/ProxyEgress storm on 3.8.49. Chat wires the cooldown + * write (`markAccountUnavailable`) on hard failures; embed never does. + * + * Repro: create a real mistral apikey connection, mock `globalThis.fetch` to + * return HTTP 402 with a payment-required JSON body, call `handleEmbedding` + * with that connectionId, then assert the connection's `rate_limited_until` + * becomes a future timestamp. Today it stays `undefined` (RED); with the fix + * `markAccountUnavailable` persists a 1h QUOTA_EXHAUSTED cooldown (GREEN). + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-embed-402-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const { handleEmbedding } = await import("../../open-sse/handlers/embeddings.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +function readConnectionRow(connId: string) { + const db = core.getDbInstance() as unknown as { + prepare: (sql: string) => { + get: (id: string) => { + test_status: unknown; + rate_limited_until: unknown; + last_error_type: unknown; + } | undefined; + }; + }; + return db + .prepare( + "SELECT test_status, rate_limited_until, last_error_type FROM provider_connections WHERE id = ?" + ) + .get(connId); +} + +test("embed 402 marks the connection terminal credits_exhausted (stops re-selection)", async () => { + const conn = await providersDb.createProviderConnection({ + provider: "mistral", + authType: "apikey", + name: "embed 402 cooldown", + }); + const connId = (conn as { id: string }).id; + + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response( + JSON.stringify({ + code: "subscription_inactive", + message: "Check your subscription", + }), + { + status: 402, + headers: { "content-type": "application/json" }, + } + ); + + try { + const result = await handleEmbedding({ + body: { model: "mistral/mistral-embed", input: "ping" }, + credentials: { apiKey: "mistral-key" }, + connectionId: connId, + log: null, + }); + + // The upstream was hit and surfaced a 402 — the bug scope. + assert.equal(result.success, false); + assert.equal(result.status, 402); + + const row = readConnectionRow(connId); + // markAccountUnavailable classifies a payment-required 402 as the TERMINAL state + // credits_exhausted (last_error_type quota_exhausted) with no transient numeric + // cooldown — the terminal marker is what excludes the account from the embed + // selection path on the next request, stopping the repeat re-hit storm. + assert.equal( + row?.test_status, + "credits_exhausted", + `expected the 402 to mark the connection terminal (test_status=credits_exhausted) on ${connId}, got ${String( + row?.test_status + )}` + ); + assert.equal( + row?.last_error_type, + "quota_exhausted", + `expected last_error_type=quota_exhausted on ${connId}, got ${String(row?.last_error_type)}` + ); + } finally { + globalThis.fetch = originalFetch; + } +}); \ No newline at end of file From dcfbc24625de447cdb5a9c809e7d0d6ab47f8f62 Mon Sep 17 00:00:00 2001 From: Dave Cox <113376598+dcox79@users.noreply.github.com> Date: Mon, 17 Aug 2026 06:59:52 -0400 Subject: [PATCH 15/22] fix(deps): pin onnxruntime-node to the exact version @huggingface/transformers requires (#10543) `@huggingface/transformers` 4.2.0 hard-pins `onnxruntime-node` to "1.24.3". The production-group bump in #10403 raised the root range from "~1.24.3" to "~1.27.0", so npm stopped deduping and nested a second copy under `node_modules/@huggingface/transformers/node_modules/onnxruntime-node`. Both copies ship a native `libonnxruntime.so.1` under the SAME SONAME, so glibc binds whichever is dlopen()ed first and the other addon dies. The Dockerfile post-build verification imports `@huggingface/transformers` and `onnxruntime-node` in one process, so `docker build` has failed on every commit since #10403: Error: .../transformers/node_modules/onnxruntime-node/bin/napi-v6/linux/x64/libonnxruntime.so.1: version `VERS_1.27.0' not found (required by .../onnxruntime-node/bin/napi-v6/linux/x64/onnxruntime_binding.node) Restore the root range to "~1.24.3" so a single hoisted copy is resolved again. Copying the nested native binaries into the standalone bundle is NOT a workaround: it makes both `.so` files present, which is precisely what triggers the SONAME clash above (verified against a real image build). Regression guard: tests/unit/onnxruntime-single-copy.test.ts asserts the lockfile resolves exactly one onnxruntime-node and that it matches the version transformers pins. Confirmed failing on the pre-fix lockfile (two copies, 1.27.0 vs 1.24.3) and passing after. Validated with a full `docker build --target runner-base`: the post-build verification step now passes (#19 DONE 156.9s) and the image boots healthy (/api/monitoring/health 200, migrations 134-148 applied). --- package-lock.json | 151 ++++----------------- package.json | 2 +- tests/unit/onnxruntime-single-copy.test.ts | 72 ++++++++++ 3 files changed, 102 insertions(+), 123 deletions(-) create mode 100644 tests/unit/onnxruntime-single-copy.test.ts diff --git a/package-lock.json b/package-lock.json index f08d7a25ec..4769372f63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "next-themes": "^0.4.6", "node-machine-id": "^1.1.12", "omniglyph": "^1.0.2", - "onnxruntime-node": "~1.27.0", + "onnxruntime-node": "~1.24.3", "open": "^11.0.0", "ora": "^9.4.1", "parse5": "^8.0.1", @@ -88,7 +88,6 @@ "undici": "^8.10.0", "update-notifier": "^7.3.1", "uuid": "^14.0.0", - "wreq-js": "3.0.0", "ws": "^8.21.3", "xxhash-wasm": "^1.1.0", "yazl": "^3.3.1", @@ -110,7 +109,7 @@ "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@types/better-sqlite3": "^9.6.0", - "@types/bun": "*", + "@types/bun": "latest", "@types/node": "^26.2.0", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", @@ -3506,97 +3505,6 @@ "sharp": "^0.34.5" } }, - "node_modules/@huggingface/transformers/node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/@huggingface/transformers/node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@huggingface/transformers/node_modules/onnxruntime-common": { - "version": "1.24.3", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", - "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", - "license": "MIT" - }, - "node_modules/@huggingface/transformers/node_modules/onnxruntime-node": { - "version": "1.24.3", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", - "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", - "hasInstallScript": true, - "license": "MIT", - "os": [ - "win32", - "darwin", - "linux" - ], - "dependencies": { - "adm-zip": "^0.5.16", - "global-agent": "^3.0.0", - "onnxruntime-common": "1.24.3" - } - }, - "node_modules/@huggingface/transformers/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@huggingface/transformers/node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@huggingface/transformers/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -20557,15 +20465,17 @@ } }, "node_modules/global-agent": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", - "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "license": "BSD-3-Clause", "dependencies": { - "globalthis": "^1.0.2", - "matcher": "^4.0.0", - "semver": "^7.3.5", - "serialize-error": "^8.1.0" + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" }, "engines": { "node": ">=10.0" @@ -26020,18 +25930,15 @@ } }, "node_modules/matcher": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", - "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "license": "MIT", "dependencies": { "escape-string-regexp": "^4.0.0" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/material-symbols": { @@ -29061,15 +28968,15 @@ } }, "node_modules/onnxruntime-common": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz", - "integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", "license": "MIT" }, "node_modules/onnxruntime-node": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.27.0.tgz", - "integrity": "sha512-QEzGwrvNBgv4uPVdnbHsOGG4G6T96mdlcFI8aAKPjMU8wOPpVocPXb6k3QGkaZagVTv2G9Bnnbo6Z3JdXr1fQw==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", "hasInstallScript": true, "license": "MIT", "os": [ @@ -29079,8 +28986,8 @@ ], "dependencies": { "adm-zip": "^0.5.16", - "global-agent": "^4.1.3", - "onnxruntime-common": "1.27.0" + "global-agent": "^3.0.0", + "onnxruntime-common": "1.24.3" } }, "node_modules/onnxruntime-web": { @@ -33011,12 +32918,12 @@ } }, "node_modules/serialize-error": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", - "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "type-fest": "^0.13.1" }, "engines": { "node": ">=10" @@ -33026,9 +32933,9 @@ } }, "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 50c57e2aaf..a4998e690f 100644 --- a/package.json +++ b/package.json @@ -337,7 +337,7 @@ "zod": "^4.4.3", "zustand": "^5.0.13", "@huggingface/transformers": "^4.2.0", - "onnxruntime-node": "~1.27.0" + "onnxruntime-node": "~1.24.3" }, "optionalDependencies": { "@atjsh/llmlingua-2": "2.0.3", diff --git a/tests/unit/onnxruntime-single-copy.test.ts b/tests/unit/onnxruntime-single-copy.test.ts new file mode 100644 index 0000000000..d988cdaeb0 --- /dev/null +++ b/tests/unit/onnxruntime-single-copy.test.ts @@ -0,0 +1,72 @@ +/** + * Regression guard — the dependency tree must resolve exactly ONE + * `onnxruntime-node` (and one `onnxruntime-common`). + * + * `@huggingface/transformers` pins `onnxruntime-node` to an EXACT version + * (4.2.0 → "1.24.3"). Whenever the root range in package.json drifts off that + * pin, npm nests a second copy under + * `node_modules/@huggingface/transformers/node_modules/onnxruntime-node`. + * + * Two copies cannot coexist in one Node process: both ship a native + * `libonnxruntime.so.1` under the SAME SONAME, so glibc's loader binds + * whichever was dlopen()ed first and the other addon dies with + * + * Error: .../libonnxruntime.so.1: version `VERS_1.27.0' not found + * (required by .../onnxruntime_binding.node) + * + * That is exactly what a production-group dependabot bump did on 2026-08-16 + * (root `onnxruntime-node` "~1.24.3" → "~1.27.0"): it broke the Docker image + * build at the Dockerfile's post-build standalone verification step, which + * imports `@huggingface/transformers` and `onnxruntime-node` in one process. + * + * Keep the root range compatible with whatever `@huggingface/transformers` + * pins — do not "fix" a future recurrence by copying the nested native + * binaries into the bundle; the SONAME clash makes that impossible. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +const lockfile = JSON.parse(readFileSync(join(repoRoot, "package-lock.json"), "utf8")) as { + packages: Record }>; +}; + +function copiesOf(pkg: string): string[] { + return Object.keys(lockfile.packages).filter( + (key) => key === `node_modules/${pkg}` || key.endsWith(`/node_modules/${pkg}`) + ); +} + +// Scoped to `onnxruntime-node` on purpose. `onnxruntime-common` is types/interfaces +// only and `onnxruntime-web` is WASM — neither dlopen()s anything, so their nested +// duplicates (onnxruntime-web carries its own onnxruntime-common) are harmless. +// `onnxruntime-node` is the sole package shipping the native libonnxruntime.so.1. +test("package-lock.json resolves exactly one copy of onnxruntime-node", () => { + assert.deepEqual( + copiesOf("onnxruntime-node"), + ["node_modules/onnxruntime-node"], + "onnxruntime-node must resolve to a single hoisted copy — a nested duplicate ships a " + + "second libonnxruntime.so.1 under the same SONAME and breaks the standalone/Docker build" + ); +}); + +test("root onnxruntime-node matches the exact version @huggingface/transformers pins", () => { + const transformers = lockfile.packages["node_modules/@huggingface/transformers"]; + assert.ok(transformers, "@huggingface/transformers must be present in the lockfile"); + + const pinned = transformers.dependencies?.["onnxruntime-node"]; + assert.ok(pinned, "@huggingface/transformers must declare an onnxruntime-node dependency"); + + const resolved = lockfile.packages["node_modules/onnxruntime-node"]?.version; + assert.equal( + resolved, + pinned, + `the hoisted onnxruntime-node (${resolved}) must equal the version ` + + `@huggingface/transformers pins (${pinned}); otherwise npm nests a second, ` + + `ABI-incompatible native copy` + ); +}); From 4540d303d74194d4a3adc4d3e313727ef3c5ec44 Mon Sep 17 00:00:00 2001 From: stanley Date: Mon, 17 Aug 2026 18:01:45 +0700 Subject: [PATCH 16/22] fix(oauth): send required CLI headers in claude-auth import bootstrap call (#10144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(oauth): send required CLI headers in claude-auth import bootstrap call enrichWithBootstrap() in claudeAuthImport.ts was missing the User-Agent and anthropic-beta headers that the two other callers of the same /api/claude_cli/bootstrap endpoint (claudeIdentity.ts and src/lib/oauth/providers/claude.ts) always send. Without them, Anthropic doesn't recognize the request as coming from a CLI client and the bootstrap call fails, silently returning a null identity (accountUUID/organizationUUID/organizationType all null). createConnectionFromAuthFile()'s identity-verification refusal then gets bypassed via overwriteExisting: true (the only way imports currently succeed, since first attempts fail with identity_unverified because of this same bug), so every imported Claude connection ends up with unverified identity. Downstream, resolveAccountUUID() in claudeIdentity.ts falls back to a hash-derived fake UUID when providerSpecificData.accountUUID is null. That fake UUID is shape-valid but was never associated with the real account by Anthropic, so requests carrying it get classified as unrecognized third-party traffic and routed to the separate extra-usage pool instead of the account's plan limits -- producing an intermittent (~50% observed) 400: "Third-party apps now draw from your extra usage, not your plan limits." on an otherwise perfectly valid, imported subscription token. Fixes the header mismatch so bootstrap succeeds and imported connections get a real, Anthropic-recognized account identity from the start, same as connections created via the native OAuth flow. Fixes #10143 * fix(oauth): persist cliUserID device identity on claude-auth import createConnectionFromAuthFile() in claudeAuthImport.ts never set providerSpecificData.cliUserID, unlike the native OAuth setup flow in src/lib/oauth/providers/claude.ts which always mints one. cliUserID is read by resolveCliUserID() (open-sse/executors/claudeIdentity.ts) as the request's device_id; when absent it falls back to a lazy-random device id regenerated fresh every process restart (in-memory Map, process-lifetime only), so every restart of an imported connection presents as a brand-new device to Anthropic for the same account -- a second, independent contributor (alongside Part 1's bootstrap header fix in this same PR) to the intermittent third-party-usage 400 on valid imported subscription tokens. - "create new connection" branch: always mint a fresh cliUserID. - "update existing connection" branch: preserve any already-persisted cliUserID from existing.providerSpecificData (don't rotate a working device identity on re-import); only mint a fresh one if absent. Adds changelog.d/fixes/10144-claude-import-cli-user-id.md per CONTRIBUTING.md. Fixes #10143 * test(oauth): cover claude-auth import bootstrap headers + cliUserID persistence Adds tests/unit/claudeAuthImport-bootstrap-headers-10144.test.ts (Rule #18 regression guard for #10143): 1. enrichWithBootstrap() sends the required CLI headers on the /api/claude_cli/bootstrap call — a claude-cli User-Agent (now sourced from CLAUDE_CODE_CLIENT_VERSION, matching the two working call-sites) and anthropic-beta: oauth-2025-04-20 — and still falls back to null identity fields on non-OK upstream responses. 2. createConnectionFromAuthFile() mints a 64-hex cliUserID device identity on create, preserves an already-persisted cliUserID on overwrite re-import (no rotation), and mints a fresh one when the existing connection has none. Also aligns the hardcoded claude-cli/1.0.0 User-Agent in the import bootstrap with the version constant the two working call-sites (claudeIdentity.ts, oauth/providers/claude.ts) already use. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(oauth): source claude-auth import UA from canonical constant (#10144 review nit) Addresses the hardcoded-version nit from review: the bootstrap User-Agent was re-typed as `claude-cli/${CLAUDE_CODE_CLIENT_VERSION}` instead of importing getClaudeCodeUserAgent() — the single source of truth the two working call-sites (claudeIdentity.ts, oauth/providers/claude.ts) use. - claudeAuthImport.ts: use getClaudeCodeUserAgent("cli") for the bootstrap call - test: import the same canonical helper instead of a local copy of the pinned version, and assert the outbound UA byte-for-byte against it, so a future version bump can't silently desync the wire identity. Verified: node --import tsx/esm --test on the new test file -> 5/5 pass; sibling claudeAuthImport.test.ts -> pass; eslint on both changed files -> no new findings (only the pre-existing @/lib/localDb barrel-import restriction on an untouched import line). * test(oauth): exercise claude auth import implementation Replace copied helper tests with real implementation coverage for bootstrap headers and persistent cliUserID behavior. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: stanleytejakusuma --- .../fixes/10144-claude-import-cli-user-id.md | 1 + src/lib/oauth/utils/claudeAuthImport.ts | 14 +++ ...AuthImport-bootstrap-headers-10144.test.ts | 114 ++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 changelog.d/fixes/10144-claude-import-cli-user-id.md create mode 100644 tests/unit/claudeAuthImport-bootstrap-headers-10144.test.ts diff --git a/changelog.d/fixes/10144-claude-import-cli-user-id.md b/changelog.d/fixes/10144-claude-import-cli-user-id.md new file mode 100644 index 0000000000..0c892de04b --- /dev/null +++ b/changelog.d/fixes/10144-claude-import-cli-user-id.md @@ -0,0 +1 @@ +- **fix(oauth):** Claude connections created via `claude-auth/import` now send required CLI headers on the bootstrap identity call and persist a `cliUserID` device identity, fixing intermittent "Third-party apps now draw from your extra usage" 400s on otherwise valid imported subscription tokens ([#10144](https://github.com/diegosouzapw/OmniRoute/pull/10144), fixes [#10143](https://github.com/diegosouzapw/OmniRoute/issues/10143)) diff --git a/src/lib/oauth/utils/claudeAuthImport.ts b/src/lib/oauth/utils/claudeAuthImport.ts index a9baf78ac9..fb0a6aa000 100644 --- a/src/lib/oauth/utils/claudeAuthImport.ts +++ b/src/lib/oauth/utils/claudeAuthImport.ts @@ -1,8 +1,10 @@ +import crypto from "node:crypto"; import { getProviderConnections, createProviderConnection, updateProviderConnection, } from "@/lib/localDb"; +import { getClaudeCodeUserAgent } from "@/shared/constants/claudeCodeClient"; import { ClaudeAuthFileError } from "@/lib/oauth/utils/claudeAuthFile"; type JsonRecord = Record; @@ -119,6 +121,8 @@ export async function enrichWithBootstrap( Authorization: `Bearer ${parsed.accessToken}`, "anthropic-version": "2023-06-01", "Content-Type": "application/json", + "User-Agent": getClaudeCodeUserAgent("cli"), + "anthropic-beta": "oauth-2025-04-20", }, signal: controller.signal, }); @@ -212,6 +216,12 @@ export async function createConnectionFromAuthFile( subscriptionType: enriched.subscriptionType, bootstrapEmail: enriched.email, importedAt: new Date().toISOString(), + // #10143: preserve an already-persisted device identity across + // re-imports so the connection doesn't present as a new device to + // Anthropic on every process restart; only mint one if absent. + cliUserID: + toNonEmptyString(toRecord(existing.providerSpecificData).cliUserID) || + crypto.randomBytes(32).toString("hex"), }, }); @@ -252,6 +262,10 @@ export async function createConnectionFromAuthFile( subscriptionType: enriched.subscriptionType, bootstrapEmail: enriched.email, importedAt: new Date().toISOString(), + // #10143: mint a persistent device identity so this imported + // connection doesn't fall back to a lazy-random device id that + // regenerates on every process restart (see resolveCliUserID). + cliUserID: crypto.randomBytes(32).toString("hex"), }, }); diff --git a/tests/unit/claudeAuthImport-bootstrap-headers-10144.test.ts b/tests/unit/claudeAuthImport-bootstrap-headers-10144.test.ts new file mode 100644 index 0000000000..e373732908 --- /dev/null +++ b/tests/unit/claudeAuthImport-bootstrap-headers-10144.test.ts @@ -0,0 +1,114 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +// The production import helper reaches the real SQLite provider module. Give +// this file its own database even when it is run without the package harness. +const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-claude-import-10144-")); +process.env.DATA_DIR = testDataDir; +process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; +process.env.APP_LOG_TO_FILE = "false"; + +// Import the implementation under test. In particular, do not copy any of +// these helpers here: the regression must fail if claudeAuthImport.ts loses a +// required header or stops persisting the device identity. +const { + createConnectionFromAuthFile, + enrichWithBootstrap, + parseAndValidateClaudeAuth, +} = await import("../../src/lib/oauth/utils/claudeAuthImport.ts"); +import { getClaudeCodeUserAgent } from "../../src/shared/constants/claudeCodeClient.ts"; + +const originalFetch = globalThis.fetch; + +test.afterEach(() => { + globalThis.fetch = originalFetch; +}); + +test.after(() => { + fs.rmSync(testDataDir, { recursive: true, force: true }); +}); + +test("real enrichWithBootstrap sends the required CLI headers", async () => { + const captured: { url: string; headers: Headers } = { + url: "", + headers: new Headers(), + }; + + globalThis.fetch = (async (input, init) => { + captured.url = String(input); + captured.headers = new Headers(init?.headers); + return new Response( + JSON.stringify({ + account_uuid: "unit-account-10144", + organization_uuid: "unit-org-10144", + organization_name: "Unit Test Organization", + organization_type: "team", + rate_limit_tier: "default", + account_email: "unit-10144@example.invalid", + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + }) as typeof fetch; + + const parsed = parseAndValidateClaudeAuth({ + claudeAiOauth: { + accessToken: "unit-test-access-token", + refreshToken: "unit-test-refresh-token", + scopes: ["user:inference"], + }, + }); + const enriched = await enrichWithBootstrap(parsed); + + assert.equal(captured.url, "https://api.anthropic.com/api/claude_cli/bootstrap"); + assert.equal(captured.headers.get("authorization"), "Bearer unit-test-access-token"); + assert.equal(captured.headers.get("anthropic-version"), "2023-06-01"); + assert.equal(captured.headers.get("content-type"), "application/json"); + assert.equal(captured.headers.get("user-agent"), getClaudeCodeUserAgent("cli")); + assert.equal(captured.headers.get("anthropic-beta"), "oauth-2025-04-20"); + assert.equal(enriched.accountUUID, "unit-account-10144"); + assert.equal(enriched.email, "unit-10144@example.invalid"); +}); + +test("real createConnectionFromAuthFile persists and preserves cliUserID", async () => { + const parsed = parseAndValidateClaudeAuth({ + claudeAiOauth: { + accessToken: "unit-test-access-token", + refreshToken: "unit-test-refresh-token", + }, + }); + const enriched = { + ...parsed, + email: "unit-10144@example.invalid", + accountUUID: "unit-account-10144-persistent", + organizationUUID: null, + organizationName: null, + organizationType: null, + }; + + const created = await createConnectionFromAuthFile(enriched, {}); + assert.equal(created.created, true); + + const createdProviderSpecificData = created.connection.providerSpecificData as Record< + string, + unknown + >; + const cliUserID = createdProviderSpecificData.cliUserID; + assert.equal(typeof cliUserID, "string"); + assert.match(cliUserID as string, /^[a-f0-9]{64}$/); + + const overwritten = await createConnectionFromAuthFile( + { ...enriched, accessToken: "unit-test-access-token-rotated" }, + { overwriteExisting: true } + ); + + assert.equal(overwritten.created, false); + assert.equal(overwritten.connection.id, created.connection.id); + assert.equal( + (overwritten.connection.providerSpecificData as Record).cliUserID, + cliUserID, + "re-import must preserve the persisted device identity" + ); +}); From b1a2ff68870cb6d1d46f8b1ca02363ca65ab0f0a Mon Sep 17 00:00:00 2001 From: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:02:16 +0300 Subject: [PATCH 17/22] feat(proxy): non-destructive auto-disable mode for the proxy health scheduler (#10342) * feat(proxy): add non-destructive auto-disable mode for the proxy health scheduler PROXY_AUTO_REMOVE was the only opt-in action the background proxy health scheduler could take on a consistently failing proxy, and it deletes the row. For a manually-maintained proxy chain (multi-proxy pool/rotation, #6365) that is too destructive just to exclude a temporarily-dead member. Add PROXY_AUTO_DISABLE as a sibling flag: at the same consecutive-failure threshold it soft-disables the proxy (status "dead") instead of removing it. "dead" is already one of the statuses the pool/rotation alive-filter excludes, so a disabled proxy drops out of the active chain immediately with no other code changes. The scheduler keeps probing dead proxies on its normal interval, and the existing recovery branch (previously autoRemove-only) re-activates it automatically once it starts answering again. decision.ts's decideProxyHealthAction() gets an optional `autoDisable` input (defaults to false, so existing callers are unaffected) and a "dead" status value; scheduler.ts wires the new PROXY_AUTO_DISABLE env flag through. If both flags are set, auto-remove wins. getProxyHealthStats() now also surfaces the registry `status` so operators can see when a proxy was auto-disabled, and ProxyStatusBadge now treats the full "not alive" status set (not just the literal string "inactive") as inactive in the dashboard. * test(proxy): assert registry status in getProxyHealthStats output The non-destructive auto-disable change added the live registry status to the stats object returned by getProxyHealthStats. Align the pre-existing db-proxies-crud assertion with the intended output shape. Co-authored-by: diegosouzapw * fix(proxy): preserve auto-disabled status in dashboard edits Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: adevwithpurpose Co-authored-by: Gi99lin Co-authored-by: diegosouzapw Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .env.example | 7 ++ docs/ops/PROXY_GUIDE.md | 44 ++++++++ docs/reference/ENVIRONMENT.md | 1 + .../components/ProxyRegistryManager.tsx | 8 +- .../settings/components/ProxyStatusBadge.tsx | 9 +- src/lib/db/proxies.ts | 12 ++- src/lib/proxyHealth/decision.ts | 51 ++++++--- src/lib/proxyHealth/scheduler.ts | 52 ++++++--- src/shared/validation/schemas/proxy.ts | 2 +- tests/unit/db-proxies-crud.test.ts | 1 + ...proxy-health-auto-disable-decision.test.ts | 102 ++++++++++++++++++ tests/unit/proxy-registry.test.ts | 13 ++- ...gistryManager-credential-autofill.test.tsx | 70 +++++++++++- 13 files changed, 336 insertions(+), 36 deletions(-) create mode 100644 tests/unit/proxy-health-auto-disable-decision.test.ts diff --git a/.env.example b/.env.example index d415d6b3c7..483dd426ca 100644 --- a/.env.example +++ b/.env.example @@ -1894,6 +1894,13 @@ APP_LOG_TO_FILE=true # PROXY_AUTO_REMOVE=false # Consecutive failures before an auto-remove fires. Default: 3. # PROXY_AUTO_REMOVE_AFTER=3 +# Set "true" to let the scheduler auto-disable (status "dead") proxies after +# repeated failures instead of deleting them. Non-destructive alternative to +# PROXY_AUTO_REMOVE — the row stays in the registry, drops out of pool/rotation +# resolution immediately, and is automatically re-activated once it starts +# answering probes again. Shares the PROXY_AUTO_REMOVE_AFTER threshold above. +# If both PROXY_AUTO_REMOVE and PROXY_AUTO_DISABLE are "true", auto-remove wins. +# PROXY_AUTO_DISABLE=false # Let automated reachability probes (the scheduler + the "Test All" button) WRITE # a proxy's status. Default "false": probes are read-only and never deactivate a # proxy — only the operator sets active/inactive (a flaky probe must not strand an diff --git a/docs/ops/PROXY_GUIDE.md b/docs/ops/PROXY_GUIDE.md index 81535d1cbf..075759fad0 100644 --- a/docs/ops/PROXY_GUIDE.md +++ b/docs/ops/PROXY_GUIDE.md @@ -817,6 +817,50 @@ The proxy is **not deleted** — it's marked unhealthy and won't be selected unt --- +## Automatic Failure Exclusion for Your Own Proxies + +`failOneproxyProxy()` above only covers the 1proxy marketplace pool, which already +auto-degrades on failure (see [Proxy Quality Scores](#proxy-quality-scores)). For +proxies **you** added to the registry, the background health scheduler +(`src/lib/proxyHealth/scheduler.ts`) provides the same "exclude a dead member from +the chain automatically" behavior, without deleting anything: + +```bash +# .env — soft-disable a proxy after 3 consecutive failed probes, re-enable it +# automatically once it starts answering probes again. +PROXY_AUTO_DISABLE=true +PROXY_AUTO_REMOVE_AFTER=3 +``` + +How it fits into a multi-proxy chain: + +1. The scheduler probes every registered proxy every `PROXY_HEALTH_INTERVAL_MS` + (default 10 min; minimum 1 min). +2. After `PROXY_AUTO_REMOVE_AFTER` consecutive **conclusive** failures (a real + connection failure — a timeout or the probe target's own 5xx never counts, see + [Proxy Health Checking](#proxy-health-checking-v3816)), the proxy's `status` is + set to `dead`. +3. `dead` is one of the statuses the alive-status filter used by pool/rotation + resolution excludes, so a scope's rotation (round-robin / random / sticky / + latency — see [Rotation Strategy Decision Tree](#rotation-strategy-decision-tree)) + immediately stops handing that proxy to new requests. No other proxies in the + pool are affected, and the whole pool never silently falls back to a direct + connection — see the [4-Level Proxy System](#4-level-proxy-system) fail-closed + guard. +4. The scheduler keeps probing `dead` proxies on the same interval. The next + successful probe flips `status` back to `active` and it re-enters rotation — + no manual re-add required. + +This is deliberately **opt-in and non-destructive**: by default the scheduler only +counts and logs failures (see policy C in `decision.ts`), and `PROXY_AUTO_DISABLE` +never deletes a row — that is what the separate, more aggressive +`PROXY_AUTO_REMOVE` flag is for. If both are set to `true`, `PROXY_AUTO_REMOVE` +wins (a proxy about to be deleted has no use for a soft-disable in between). See +the [Environment Config](../reference/ENVIRONMENT.md) reference for the full +variable list. + +--- + > 📖 **Related documentation:** > > - [User Guide](../guides/USER_GUIDE.md) — General setup and configuration diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 6923092247..7c39edd8d1 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -999,6 +999,7 @@ Anthropic-compatible provider instead. | `PROXY_HEALTH_AUTO_DEACTIVATE` | `false` | `src/lib/proxyHealth/statusPolicy.ts` | When `false` (default), automated reachability probes (the scheduler + the `/api/settings/proxies/auto-test` "Test All" button) are **read-only** and never write a proxy's status — only the operator sets active/inactive, so a flaky probe can't strand an assigned proxy (#6246). Set `true` to restore the legacy test-and-set behaviour. | | `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. | | `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). | +| `PROXY_AUTO_DISABLE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler soft-disable (status `dead`, never deleted) a proxy after repeated consecutive failures, instead of removing it. Non-destructive alternative to `PROXY_AUTO_REMOVE`: the proxy drops out of pool/rotation resolution immediately (the alive-status filter used by scope-pool resolution already excludes it) and is automatically re-activated once it starts passing probes again. Shares the `PROXY_AUTO_REMOVE_AFTER` threshold. If both flags are `true`, `PROXY_AUTO_REMOVE` wins. | | `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Effective precedence is Feature Flags DB override > env var > default. | | `RATE_LIMIT_MAX_WAIT_MS` | `15000` (15s) | `open-sse/services/rateLimitManager.ts` | Max time to wait on a 429 before failing the request. | | `RATE_LIMIT_MAX_QUEUE_DEPTH` | `0` (disabled) | `open-sse/services/rateLimitManager.ts` | Queue admission cap: reject with a 429 `queue_full` once this many requests are already queued. `0` = unbounded (default). | diff --git a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx index 47d0334041..990d17988d 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx @@ -1046,9 +1046,11 @@ import { className="w-full px-3 py-2 rounded bg-bg-subtle border border-border" value={form.status} onChange={(e) => setForm((prev) => ({ ...prev, status: e.target.value }))} + data-testid="proxy-registry-status-select" > + {form.status === "dead" && } @@ -1281,7 +1283,11 @@ import { > {items - .filter((item) => !poolMembers.includes(item.id)) + .filter( + (item) => + !poolMembers.includes(item.id) && + (item.status ?? "").toLowerCase() !== "dead" + ) .map((item) => (