diff --git a/.env.example b/.env.example
index 4e6438b593..9527957b3d 100644
--- a/.env.example
+++ b/.env.example
@@ -1290,7 +1290,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
# Update these when providers release new CLI versions to avoid blocks.
-CLAUDE_USER_AGENT="claude-cli/2.1.219 (external, cli)"
+CLAUDE_USER_AGENT="claude-cli/2.1.258 (external, cli)"
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) —
@@ -1524,6 +1524,15 @@ CURSOR_USER_AGENT="Cursor/3.4"
# request into the browser-backed path.
# OMNIROUTE_BROWSER_POOL=on
# WEB_COOKIE_USE_BROWSER=0
+# Obscura (https://github.com/h4ckf0r0day/obscura) is the primary headless
+# engine: a lightweight CDP server the pool and cloudflare-playground connect
+# to before falling back to Chromium. Unset OBSCURA_BIN to auto-detect from
+# PATH; set OBSCURA_CDP_ENDPOINT to reuse an already-running Obscura instead
+# of spawning one; set OBSCURA_PORT to pin the spawned serve port.
+# Used by: open-sse/services/obscura.ts
+# OBSCURA_BIN=
+# OBSCURA_CDP_ENDPOINT=
+# OBSCURA_PORT=
# ── Kimi Web (international kimi.ai Connect-RPC) ──
# Used by: open-sse/executors/kimi-web.ts. Override the base/chat URLs only if
diff --git a/AGENTS.md b/AGENTS.md
index d0ac2952b2..6e7ad18f2f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
## Project at a Glance
-**OmniRoute** — unified AI proxy/router. One endpoint, 355 LLM providers, auto-fallback.
+**OmniRoute** — unified AI proxy/router. One endpoint, 356 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -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 (168 migrations) |
+| Database | `src/lib/db/` | SQLite domain modules (169 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 110 tools (45 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6fd097613..bfcad514f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### ✨ New Features
+- **feat(dashboard):** adaptive context-budget dial on the compression settings panel — mode (`off` / `floor` / `replace-autotrigger`) and policy (`reserve-output` / `percentage` / `absolute`) persist via `PUT /api/settings/compression` `contextBudget`. Completes the dashboard half of #7005 (API + DB already shipped in #7183).
- **feat(sse): STRICT_ZERO_COST** — opt-in, off-by-default `freeAccessPolicy: "strict"` setting
that hard-verifies every auto-combo candidate against live quota state and per-connection
economic safety before it can be dispatched, going beyond `hidePaidModels`'s static catalog
diff --git a/README.md b/README.md
index 303b9e12b6..1471960181 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
-
-
+
### 🏢 Every major lab — through one endpoint
@@ -724,6 +721,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
📦 npm (global) npm install -g omnirouteOne command, any OS
🐳 Docker docker run … diegosouzapw/omnirouteMulti-arch AMD64 + ARM64
🖥️ Desktop (Electron) npm run electron:buildNative window + system tray — Windows / macOS / Linux
+
🎩 Menu-bar (OmniRouteTray) brew install --cask zoispag/tap/omniroute-traySupervises & auto-updates the server — macOS
💪 ARM native arm64 Raspberry Pi, ARM servers, Apple Silicon
📱 Android (Termux) pkg install nodejs && npx -y omnirouteRuns on your phone , 24/7, no root
📲 PWA "Add to Home Screen" Fullscreen, offline, installable from browser
@@ -732,7 +730,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
🛠️ From source npm install && npm run devHack on it, contribute
-
📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) · [Desktop](electron/README.md) · [Termux](docs/guides/TERMUX_GUIDE.md) · [PWA](docs/guides/PWA_GUIDE.md) · [OpenCode](docs/frameworks/OPENCODE.md)
+
📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) · [Desktop](electron/README.md) · [Menu-bar tray](https://github.com/zoispag/omniroute-tray) · [Termux](docs/guides/TERMUX_GUIDE.md) · [PWA](docs/guides/PWA_GUIDE.md) · [OpenCode](docs/frameworks/OPENCODE.md)
@@ -767,6 +765,42 @@ From inside the editor: open the **Extensions** view, search **"OmniRoute"**, cl
+### 🎩 New: OmniRouteTray — your gateway, living in the menu bar
+
+
+
+> `omniroute serve` is happiest when it's always on. **[OmniRouteTray](https://github.com/zoispag/omniroute-tray)**
+> turns that into a set-and-forget menu-bar app for macOS: it starts the server, keeps it alive
+> across reboots, updates it in place, and puts your live token budget one click away — **no
+> terminal window left open, no `npm install -g omniroute` to babysit.**
+
+Built with [Tauri v2](https://v2.tauri.app/) (a Rust core the size of a rounding error), it ships
+its own signed Node 24 runtime and manages an app-owned OmniRoute install, so it never fights your
+global `node`/`bun`. It **shares your existing `~/.omniroute/` config and database** — so it's the
+same OmniRoute you already run, just with a hat on. 🎩
+
+
+ What it does How
+ 🟢 Supervises the server Spawns omniroute serve, adopts an already-running instance instead of duplicating it
+ 📊 Live usage at a glance Provider quota bars, Claude session/weekly limits with reset countdowns, 30-day cost breakdown
+ 🔄 Auto-updates in place Staged install, atomic swap, rollback on failure — always on the newest release
+ 🚀 Start on login Optional launch at login; tray-only, no dock icon
+ 🩺 Doctor & logs One-click diagnostics and server log access
+
+
+```sh
+brew install --cask zoispag/tap/omniroute-tray
+```
+
+
Prefer a download? Grab the latest .dmg from
+Releases . Source, issues and build
+docs live at zoispag/omniroute-tray .
+ 💛 A community project by @zoispag — not an official OmniRoute release.
+
+
+
+
+
## 🔒 Private & Local-First
@@ -1207,7 +1241,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
Runtime Node.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27
Language TypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0)
Framework Next.js 16 + React 19 + Tailwind CSS 4
-
Database better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 168 migrations
+
Database better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 169 migrations
Memory SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay
Schemas Zod 4 — MCP tool I/O validation + API contracts
Protocols MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)
@@ -1270,8 +1304,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
Resilience Guide Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing
Auto-Combo Engine 16-factor scoring, mode packs, self-healing
Proxy Guide 3-level proxy system, 1proxy marketplace, registry CRUD
-
Free Tiers Consolidated directory: 35 documented recurring pools / 451 cataloged free-tier entries
-
Features Gallery Visual dashboard tour with screenshots
+
Free Tiers Consolidated directory: 35 documented recurring pools / 451 cataloged free-tier entries Features Gallery Visual dashboard tour with screenshots
Codebase Documentation Beginner-friendly codebase walkthrough
diff --git a/changelog.d/features/11786-seekai-provider.md b/changelog.d/features/11786-seekai-provider.md
new file mode 100644
index 0000000000..ad4e2dbd44
--- /dev/null
+++ b/changelog.d/features/11786-seekai-provider.md
@@ -0,0 +1 @@
+- **feat(providers):** add SeekAi (`seekai.cc`) as an OpenAI-compatible New-API gateway — catalog id `seekai` (alias `ska`), `https://seekai.cc/v1`, live `/v1/models` via `passthroughModels`, aggregator-list membership so New-API balance detection can opt in. No referral/aff codes. ([#11786](https://github.com/diegosouzapw/OmniRoute/issues/11786))
diff --git a/changelog.d/features/12071-csv-import-errors.md b/changelog.d/features/12071-csv-import-errors.md
new file mode 100644
index 0000000000..096eed4ef6
--- /dev/null
+++ b/changelog.d/features/12071-csv-import-errors.md
@@ -0,0 +1 @@
+- **feat(providers):** import-from-file modal shows per-row API errors and ships a downloadable CSV template ([#12071](https://github.com/diegosouzapw/OmniRoute/issues/12071))
diff --git a/changelog.d/features/12108-provider-search-name-baseurl.md b/changelog.d/features/12108-provider-search-name-baseurl.md
new file mode 100644
index 0000000000..a13f2a12a1
--- /dev/null
+++ b/changelog.d/features/12108-provider-search-name-baseurl.md
@@ -0,0 +1 @@
+- **feat(providers):** dashboard search matches connection name and `baseUrl` so imported OpenAI-compat nodes surface on the provider card ([#12108](https://github.com/diegosouzapw/OmniRoute/issues/12108))
diff --git a/changelog.d/features/12306-headroom-url.md b/changelog.d/features/12306-headroom-url.md
new file mode 100644
index 0000000000..7478e094f8
--- /dev/null
+++ b/changelog.d/features/12306-headroom-url.md
@@ -0,0 +1 @@
+- **feat(settings):** persist `headroomUrl` through Settings so status/start use the operator URL instead of only `HEADROOM_URL` ([#12306](https://github.com/diegosouzapw/OmniRoute/issues/12306))
diff --git a/changelog.d/fixes/12312-grok-cli-supergrok-quota.md b/changelog.d/fixes/12312-grok-cli-supergrok-quota.md
new file mode 100644
index 0000000000..1fed5df39e
--- /dev/null
+++ b/changelog.d/fixes/12312-grok-cli-supergrok-quota.md
@@ -0,0 +1 @@
+- **fix(grok-cli):** treat omitted SuperGrokPro `creditUsagePercent` as 0% used so Provider Limits still renders a weekly bar (proto3 zero-elision) ([#12312](https://github.com/diegosouzapw/OmniRoute/pull/12312)) — thanks @HouMinXi
diff --git a/changelog.d/fixes/12325-generic-quota-429-invalidate.md b/changelog.d/fixes/12325-generic-quota-429-invalidate.md
new file mode 100644
index 0000000000..753e96027d
--- /dev/null
+++ b/changelog.d/fixes/12325-generic-quota-429-invalidate.md
@@ -0,0 +1 @@
+- **fix(quota):** drop the generic quota cache (agy / Antigravity / Claude OAuth) on an upstream 429 so reset-aware scoring does not keep a 60s stale snapshot, and force-refresh the next usage fetch so inner provider caches cannot recache the same window ([#12325](https://github.com/diegosouzapw/OmniRoute/pull/12325)) — thanks @HouMinXi
diff --git a/changelog.d/fixes/12581-basereds-licenses-a2a-lifecycle.md b/changelog.d/fixes/12581-basereds-licenses-a2a-lifecycle.md
new file mode 100644
index 0000000000..8dbb345f96
--- /dev/null
+++ b/changelog.d/fixes/12581-basereds-licenses-a2a-lifecycle.md
@@ -0,0 +1 @@
+- **fix(ci):** document MIT exceptions for `@eloqnt/{config,format-json,format-po}` (next-intl transitive; locked tarballs omit `license`) and keep the A2A lifecycle vitest off the real SQLite persistence seam ([#12581](https://github.com/diegosouzapw/OmniRoute/issues/12581))
diff --git a/changelog.d/maintenance/12402-claude-code-2-1-258.md b/changelog.d/maintenance/12402-claude-code-2-1-258.md
new file mode 100644
index 0000000000..6b5ba0b108
--- /dev/null
+++ b/changelog.d/maintenance/12402-claude-code-2-1-258.md
@@ -0,0 +1 @@
+- **chore(providers):** bump the Claude Code wire identity and the Devin bridge image pin from `2.1.220` to `2.1.258` ([#12402](https://github.com/diegosouzapw/OmniRoute/pull/12402)) — thanks @ggiak
diff --git a/changelog.d/maintenance/12554-electron-44.md b/changelog.d/maintenance/12554-electron-44.md
new file mode 100644
index 0000000000..41b5e50f53
--- /dev/null
+++ b/changelog.d/maintenance/12554-electron-44.md
@@ -0,0 +1 @@
+- **chore(electron):** upgrade the desktop app to Electron 44 (Chromium 152, Node 24.18.1) ([#12217](https://github.com/diegosouzapw/OmniRoute/pull/12217)). **Requires macOS 13 (Ventura) or later** — Chromium dropped macOS 12 (Monterey), so Monterey users must stay on an earlier OmniRoute desktop build. Windows and Linux are unaffected; the app already shipped only x64/arm64, so Electron 44 dropping 32-bit builds changes nothing. Removes the `openAsHidden`/`wasOpenedAsHidden` login-item fields deleted in Electron 44 — hidden autostart continues to work through the `--hidden` argument registered with the login item ([#12554](https://github.com/diegosouzapw/OmniRoute/pull/12554))
diff --git a/changelog.d/maintenance/hartmark-batch-filesize.md b/changelog.d/maintenance/hartmark-batch-filesize.md
new file mode 100644
index 0000000000..d55f5b4029
--- /dev/null
+++ b/changelog.d/maintenance/hartmark-batch-filesize.md
@@ -0,0 +1 @@
+- **chore(quality):** rebaseline the file-size caps the hartmark batch grew past (`combos/page.tsx` via [#12355](https://github.com/diegosouzapw/OmniRoute/pull/12355), `open-sse/services/combo.ts` via [#12338](https://github.com/diegosouzapw/OmniRoute/pull/12338))
diff --git a/changelog.d/maintenance/houminxi-batch-filesize.md b/changelog.d/maintenance/houminxi-batch-filesize.md
new file mode 100644
index 0000000000..0d3426ed51
--- /dev/null
+++ b/changelog.d/maintenance/houminxi-batch-filesize.md
@@ -0,0 +1 @@
+- **chore(quality):** rebaseline the file-size caps the HouMinXi batch grew past when its PRs stacked (`providers/page.tsx`, `chatCore.ts`, `accountFallback.ts`) — each PR measured correctly in isolation, none saw the stacking
diff --git a/config/quality/.license-allowlist.json b/config/quality/.license-allowlist.json
index f2cca50374..c74ee066e4 100644
--- a/config/quality/.license-allowlist.json
+++ b/config/quality/.license-allowlist.json
@@ -74,6 +74,24 @@
"justification": "CC-BY-4.0 applies to the caniuse browser-support data (a dataset, not code). The Creative Commons Attribution license requires attribution when distributing — OmniRoute does not distribute caniuse-lite data directly to end users; it is consumed by browserslist/PostCSS at build time to generate CSS compatibility info. This is a widely accepted pattern in the Node.js ecosystem (caniuse-lite is in millions of projects). Attribution is satisfied by keeping the package in node_modules with its original license file.",
"risk": "low",
"reviewAt": "v4.0.0"
+ },
+ "@eloqnt/config": {
+ "license": "MIT",
+ "justification": "Transitive of next-intl (MIT). npm registry SPDX for the @eloqnt scope is MIT; @eloqnt/config@0.1.0 republished with license: MIT. The locked 0.0.2 tarball (next-intl's ^0.0.2 range, which is 0.0.x only) omits both package.json#license and a LICENSE file, so license-checker reports UNKNOWN. Same author (Jan Amann / amannn). OmniRoute does not modify the package. Re-review when next-intl bumps the range to a release that ships the license field.",
+ "risk": "low",
+ "reviewAt": "v4.0.0"
+ },
+ "@eloqnt/format-json": {
+ "license": "MIT",
+ "justification": "Same as @eloqnt/config: next-intl transitive, registry SPDX MIT, locked 0.0.3 tarball omits license field and LICENSE file so the checker reports UNKNOWN. Re-review with the next-intl range bump.",
+ "risk": "low",
+ "reviewAt": "v4.0.0"
+ },
+ "@eloqnt/format-po": {
+ "license": "MIT",
+ "justification": "Same as @eloqnt/config: next-intl transitive, registry SPDX MIT, locked 0.0.3 tarball omits license field and LICENSE file so the checker reports UNKNOWN. Re-review with the next-intl range bump.",
+ "risk": "low",
+ "reviewAt": "v4.0.0"
}
}
}
diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json
index 90f949af47..e10d1f6551 100644
--- a/config/quality/eslint-suppressions.json
+++ b/config/quality/eslint-suppressions.json
@@ -220,11 +220,6 @@
"count": 26
}
},
- "open-sse/handlers/chatCore/clientUsageBuffer.ts": {
- "@typescript-eslint/no-unused-vars": {
- "count": 1
- }
- },
"open-sse/handlers/chatCore/executorHelpers.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -608,11 +603,6 @@
"count": 1
}
},
- "open-sse/services/providerCostData.ts": {
- "@typescript-eslint/no-unused-vars": {
- "count": 1
- }
- },
"open-sse/services/rateLimitManager.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 2
@@ -763,7 +753,7 @@
},
"open-sse/utils/cursorAgentProtobuf.ts": {
"@typescript-eslint/no-unused-vars": {
- "count": 3
+ "count": 2
}
},
"open-sse/utils/earlyStreamKeepalive.ts": {
@@ -1722,16 +1712,6 @@
"count": 2
}
},
- "src/lib/oneproxyRotator.ts": {
- "@typescript-eslint/no-unused-vars": {
- "count": 1
- }
- },
- "src/lib/oneproxySync.ts": {
- "@typescript-eslint/no-unused-vars": {
- "count": 1
- }
- },
"src/lib/piiSanitizer.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -4812,7 +4792,7 @@
},
"tests/unit/responses-translation-fixes.test.ts": {
"@typescript-eslint/no-explicit-any": {
- "count": 35
+ "count": 34
}
},
"tests/unit/route-edge-coverage.test.ts": {
diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index c4e3bd4361..c39d3d5d41 100644
--- a/config/quality/file-size-baseline.json
+++ b/config/quality/file-size-baseline.json
@@ -1,4 +1,7 @@
{
+ "_rebaseline_2026_09_03_moonshot_native_quota": "PR feat/moonshot-native-quota own growth on release/v3.8.51: src/lib/db/migrationRunner.ts 1201->1206 (+5, case 172 retroactive guard for daily_quota_reset_* columns); src/sse/handlers/chat.ts 2434->2450 (+16, registerMoonshotQuotaFetcher + startup node scan at the existing quota-fetcher registration chokepoint); src/sse/services/auth.ts 3427->3450 (+23, resolveDailyResetForProvider + dailyReset arg on checkFallbackError); open-sse/services/accountFallback.ts 2422->2461 (+39, compatible-node credits_exhausted carve-out + TPD node-clock lock); tests/unit/account-fallback-service.test.ts 2008->2056 (+48, TPD/empty-wallet cases). Wiring at existing chokepoints; Moonshot host predicates, daily reset clock, and the balance fetcher live in new leaves under cap. Covered by tests/unit/moonshot-*.test.ts + account-fallback-service.test.ts (135/135 focused).",
+ "_rebaseline_2026_09_02_11786_seekai_provider": "PR #11786 (feat/11786-seekai-provider, closes #11786) own growth: src/shared/constants/providers/apikey/gateways.ts 1438->1458 (check-file-size split-newline=1459; the seekai APIKEY_PROVIDERS_GATEWAYS catalog entry plus authHint, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines: #10987 logfare, #10531 freebuff). Covered by tests/unit/seekai-provider.test.ts.",
+ "_rebaseline_2026_09_02_12325_generic_429_invalidate": "PR #12325 own growth: open-sse/handlers/chatCore.ts 5946->5955 (+9 = the non-Codex 429 else-if that drops the generic quota wrapper and stamps force-refresh, plus a source-regex breadcrumb). Irreducible call-site wiring next to the existing Codex 429 invalidateCodexQuotaCache branch; not extractable without splitting handleChatCore mid-response. Covered by tests/unit/generic-quota-fetcher.test.ts (31/31) and tests/unit/antigravity-429-quota-cooldown.test.ts.",
"_rebaseline_2026_09_02_12429_wreq_migration_suite": "PR #12429 (wreq-js web-cookie transport): new test file tests/unit/tls-client-wreq-migration.test.ts at 1374 lines, above the 1200 new-file testCap. Frozen rather than split: it is the single cohesive regression suite for the transport migration (31 cases covering streaming, fragmented EOF sentinels, proxy isolation, first-byte and hard deadlines, binary responses and cancellation), and the cases share the native-transport harness the file sets up once. Splitting it during a merge would duplicate that harness across files for no coverage gain. Entered at the exact LOC, so it can only ratchet down from here.",
"_rebaseline_2026_09_02_12239_chatgpt_web_cleanroom": "PR #12239 (backryun, codex/restore-chatgpt-web-cleanroom) own growth at the two existing chat chokepoints for the clean-room ChatGPT Web transport: src/sse/handlers/chat.ts 2384->2424 (+40); open-sse/handlers/chatCore.ts 5946->5976 (+30). Additive dispatch wiring; the retirement guard is narrowed to the GPL-derived cgpt-web alias rather than removed, so #11754's provenance decision still holds for the old implementation. Same own-growth rationale as _rebaseline_2026_08_20_10531_freebuff_provider.",
"_rebaseline_2026_09_02_12412_grok_web_prettier": "PR #12412 (repository Prettier style applied to tests/unit/grok-web.test.ts): the reformat expands the file +277 lines (2436 -> 2713) with an identical parsed AST — no production code, no assertion changes. Cap set to 2985 rather than the exact 2713 on the operator's instruction (2026-09-02): ~10% headroom so routine additions to this suite do not re-trip the gate on formatting alone. Previous cap 2437. This is a deliberate exception to the down-only ratchet for one reformatted test file; every other entry keeps the #12411 tightening.",
@@ -204,7 +207,7 @@
"_rebaseline_basered_codebuddy_cn": "Base-red fix (#4664 CodeBuddy CN): oauth-providers-config.test.ts 867->870 (+3) to align the EXPECTED provider list/config with the codebuddy-cn provider that #4664 added to the registry without updating this test (it asserts 'exactly once').",
"_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.",
"tests/integration/chat-pipeline.test.ts": 1644,
- "tests/unit/account-fallback-service.test.ts": 2008,
+ "tests/unit/account-fallback-service.test.ts": 2056,
"tests/unit/batch_api.test.ts": 1345,
"tests/unit/cc-compatible-provider.test.ts": 1225,
"tests/unit/chatcore-translation-paths.test.ts": 3447,
@@ -412,14 +415,14 @@
"open-sse/executors/codex.ts": 1499,
"open-sse/executors/cursor.ts": 1759,
"open-sse/executors/muse-spark-web.ts": 1405,
- "open-sse/handlers/chatCore.ts": 5976,
+ "open-sse/handlers/chatCore.ts": 5984,
"open-sse/handlers/imageGeneration.ts": 3259,
"open-sse/handlers/search.ts": 1789,
"open-sse/mcp-server/schemas/tools.ts": 1621,
"open-sse/mcp-server/server.ts": 1572,
- "open-sse/services/accountFallback.ts": 2422,
+ "open-sse/services/accountFallback.ts": 2467,
"open-sse/services/adobeFireflyBrowserLogin.ts": 1401,
- "open-sse/services/combo.ts": 4023,
+ "open-sse/services/combo.ts": 4036,
"open-sse/translator/response/openai-responses.ts": 1466,
"open-sse/utils/cursorAgentProtobuf.ts": 1547,
"open-sse/utils/proxyFetch.ts": 1271,
@@ -428,15 +431,15 @@
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1322,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1344,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3186,
- "src/app/(dashboard)/dashboard/combos/page.tsx": 5012,
+ "src/app/(dashboard)/dashboard/combos/page.tsx": 5018,
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1319,
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2491,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1631,
- "src/app/(dashboard)/dashboard/providers/page.tsx": 2007,
+ "src/app/(dashboard)/dashboard/providers/page.tsx": 2025,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1475,
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1271,
- "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1606,
+ "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1607,
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1597,
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2152,
"src/app/api/providers/[id]/models/route.ts": 2432,
@@ -445,14 +448,14 @@
"src/app/docs/lib/openapi.generated.ts": 1347,
"src/lib/db/apiKeys.ts": 1610,
"src/lib/db/core.ts": 1745,
- "src/lib/db/migrationRunner.ts": 1201,
+ "src/lib/db/migrationRunner.ts": 1206,
"src/lib/tailscaleTunnel.ts": 1208,
"src/lib/tokenHealthCheck.ts": 1218,
"src/shared/components/RequestLoggerV2.tsx": 1718,
- "src/shared/constants/providers/apikey/gateways.ts": 1439,
+ "src/shared/constants/providers/apikey/gateways.ts": 1459,
"src/shared/services/cliRuntime.ts": 1296,
- "src/sse/handlers/chat.ts": 2424,
- "src/sse/services/auth.ts": 3427,
+ "src/sse/handlers/chat.ts": 2450,
+ "src/sse/services/auth.ts": 3450,
"tests/unit/account-fallback-service.test.ts": 2453,
"tests/unit/provider-validation-specialty.test.ts": 4656
},
@@ -629,5 +632,9 @@
"_rebaseline_2026_06_30_v3842_release_chatgptweb_compression": "v3.8.42 cycle-close file-size reconciliation (DRIFT measured OK on each PR's base, stacked above frozen at the merge tip; fast-path PR->release/** does not run check:file-size). (1) open-sse/executors/chatgpt-web.ts 2870->3206 (+336 = #5531 portable SHA3-512 sentinel-PoW wiring with the native-vs-fallback digest path + #5536 GPT-5.5 Pro handoff branch; the pure Keccak-f[1600] fallback itself already lives in the separate leaf open-sse/utils/sha3-512.ts — the executor growth is the cohesive call-site/handoff logic, not extractable without hiding the sentinel chokepoint). (2) tests/unit/chatgpt-web.test.ts 2855->3159 (+304 = #5536 GPT-5.5 Pro handoff coverage; pair-file with its executor). (3) open-sse/services/compression/strategySelector.ts 997->1022 (+25 = #5527 T02 honest default-on pipeline inflation guard wiring at the existing finalizeStackedResult choke). All cohesive at existing chokepoints; covered by tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts, chatgpt-web.test.ts (GPT-5.5 Pro), compression-pipeline-inflation-guard.test.ts.",
"open-sse/executors/chatgpt-web.ts": "3241",
"_rebaseline_2026_08_30_11771_vercel_gateway_passthrough": "PR #11771 adds passthroughModels: true (1 line) to the Vercel AI Gateway registry entry — no split available, single-line provider-config addition.",
- "_relax_velocity_2026_08_30": "127 frozen line caps and cap/testCap raised by 20% (velocity phase; see quality-baseline.json _policy)."
+ "_relax_velocity_2026_08_30": "127 frozen line caps and cap/testCap raised by 20% (velocity phase; see quality-baseline.json _policy).",
+ "_rebaseline_2026_09_02_12325_merge_v3851": "Merge of release/v3.8.51 into #12325. Both sides grew chatCore.ts at the same chokepoint: #12239 took it 5946->5976 upstream, and this PR adds its +9 non-Codex 429 branch on top. check-file-size.mjs counts split(\"\\\\n\").length (trailing-newline empty element), so the merged file is 5981. The cap is the merged LOC, not either side alone; no other entry moves.",
+ "_rebaseline_2026_09_03_houminxi_batch_stacked": "Crescimento medido DEPOIS que os 9 PRs da leva HouMinXi entraram, quando cada um empilhou sobre o rebaseline do anterior: providers/page.tsx 2007->2025 (+18 = feedback de erro por linha do import CSV do #12504 somado a busca por nome/baseUrl do #12495, ambos no mesmo painel de conexoes); chatCore.ts 5981->5984 (+3 = o #12325 invalida o cache generico de quota no 429 upstream, ao lado do ramo Codex ja existente); accountFallback.ts 2461->2467 (+6 = o #12566 empilha a carve-out de familia Antigravity sobre o rebaseline 2422->2461 que o #12590 registrou para o carve-out credits_exhausted da Moonshot; os dois tocam checkFallbackError). Cada PR mediu certo isoladamente, mas nenhum enxergava o empilhamento. Fiacao em chokepoints existentes. NAO cobre codex.ts nem stream.ts, que ja violavam no tip antes desta leva (drift da base).",
+ "_rebaseline_2026_09_03_12604_claude_code_2_1_258": "PR #12604 (bump da wire identity do Claude Code 2.1.220->2.1.258, commits do @ggiak vindos do #12402) crescimento proprio: src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx 1606->1607 (+1, a linha do seletor que acompanha a nova versao de identidade). Uma linha num painel de settings ja existente; nao ha o que extrair. Coberto por client-identity-profiles e claude-codex-identity-version-sync (138/138 focados).",
+ "_rebaseline_2026_09_03_hartmark_batch": "Leva hartmark (#12293 #12355 #12447 #12445 #12446 #12460 #12461 #12338 #12448) crescimento proprio, medido no tip com os nove mergeados: src/app/(dashboard)/dashboard/combos/page.tsx 5012->5018 (+6, #12355 impede que a falha de bundling do tiktoken de um provider sem relacao derrube /api/providers, e o painel passa a lidar com o estado degradado); open-sse/services/combo.ts 4023->4036 (+13, #12338 nos fixes do universal-handoff: nota de bare-fallback, escopo por mesma requisicao e log da falha silenciosa). Fiacao em chokepoints existentes do roteamento de combo. NAO cobre codex.ts nem stream.ts, ja violando no tip antes desta leva (drift da base)."
}
diff --git a/docker/devin-bridge/Dockerfile b/docker/devin-bridge/Dockerfile
index 3fd2a30dba..895455cf9f 100644
--- a/docker/devin-bridge/Dockerfile
+++ b/docker/devin-bridge/Dockerfile
@@ -1,6 +1,6 @@
FROM node:26.0.0-bookworm-slim
-ARG CLAUDE_CODE_VERSION=2.1.220
+ARG CLAUDE_CODE_VERSION=2.1.258
ARG DEVIN_CLI_VERSION=3000.2.17
ARG TARGETARCH
diff --git a/docker/devin-bridge/compose.yml b/docker/devin-bridge/compose.yml
index c850414dbc..1a4ab8884b 100644
--- a/docker/devin-bridge/compose.yml
+++ b/docker/devin-bridge/compose.yml
@@ -28,7 +28,7 @@ x-runtime: &runtime
context: ../..
dockerfile: docker/devin-bridge/Dockerfile
args:
- CLAUDE_CODE_VERSION: 2.1.220
+ CLAUDE_CODE_VERSION: 2.1.258
DEVIN_CLI_VERSION: 3000.2.17
user: "10001:10001"
read_only: true
diff --git a/docs/DEVIN_CLAUDE_BRIDGE.md b/docs/DEVIN_CLAUDE_BRIDGE.md
index 4a6d56d370..30b3bf3d4c 100644
--- a/docs/DEVIN_CLAUDE_BRIDGE.md
+++ b/docs/DEVIN_CLAUDE_BRIDGE.md
@@ -4,16 +4,22 @@
Messages endpoint while the official Devin CLI supplies model responses over ACP stdio. It
does not modify the existing Anthropic, Claude OAuth, Claude Web, or `devin-cli` providers.
-> **Current status: offline and live validated.** The pinned Claude Code `2.1.220` completed
-> three isolated scenarios through Devin CLI `3000.2.17` and model
-> `swe-1-7-lightning`. The final live run proved client-owned `Read`, `Edit`, and `Bash`
-> turns, successful `npm test` results, project command and skill discovery, Devin-only
-> routing, and zero Claude egress.
+> **Current status: pinned Claude Code `2.1.258`; offline and live validation last recorded
+> on `2.1.220`.** The `2.1.220` pin completed three isolated scenarios through Devin CLI
+> `3000.2.17` and model `swe-1-7-lightning`; that final live run proved client-owned `Read`,
+> `Edit`, and `Bash` turns, successful `npm test` results, project command and skill
+> discovery, Devin-only routing, and zero Claude egress. The pin was then raised to `2.1.258`
+> (the CLI generation OmniRoute's Claude identity impersonates, and the first line that ships
+> the Fable 5.1 tier natively). On the new pin the install layer and `claude --version` were
+> verified on the pinned base image, and the bridge unit suite, `compose config` and the
+> static isolation proof pass — but the offline mock scenario and the live three-scenario
+> suite have not been re-run yet. Re-run them (see "Updating pinned tools") before relying on
+> the bridge with this pin.
## Architecture
```text
-Claude Code 2.1.220 (isolated non-root Linux container)
+Claude Code 2.1.258 (isolated non-root Linux container)
-> http://omniroute:20128/v1/messages
-> devin-cli-agentic (Claude-format, no-auth provider)
-> devin acp --agent-type summarizer (official ACP stdio, no Devin tools)
diff --git a/docs/architecture/QUALITY_GATES.md b/docs/architecture/QUALITY_GATES.md
index a3bca79c6b..4b4315cc4d 100644
--- a/docs/architecture/QUALITY_GATES.md
+++ b/docs/architecture/QUALITY_GATES.md
@@ -90,17 +90,17 @@ Runs on every PR to `main`. Blocks merge on failure.
Runs after `test-coverage`. Blocks merge on failure.
-| Script | Validates | Blocking |
-| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
-| `quality:collect` | Emits `quality-metrics.json` (ESLint warning count, coverage from merged shard report) | Yes (upstream of ratchet) |
-| `quality:ratchet` | Each metric in `quality-baseline.json` has not regressed (ESLint warnings ≤ baseline; coverage ≥ baseline) | Yes |
-| `check:duplication` | Code duplication (jscpd@4) does not exceed baseline in `quality-baseline.json` | Yes |
-| `check:complexity` | File-level cyclomatic complexity does not exceed the cap (core ESLint `complexity` + `max-lines-per-function`) | Yes |
-| `check:cognitive-complexity` | Cognitive complexity ratchet (`eslint-plugin-sonarjs`) — separate ESLint pass; CI runs both merged as the single `check:complexity-ratchets` step | Yes |
-| `check:dead-code` | Unused exports / files ratchet (knip) does not regress vs baseline | Yes |
-| `check:compression-budget` | Compression benchmark budget — per-engine token-savings floors must not regress | Yes |
-| `check:type-coverage` | Percent-typed ratchet (`type-coverage`) does not regress; largely subsumes `typecheck:noimplicit:core` | Yes |
-| `check:codeql-ratchet` | Open CodeQL alert count does not regress (reads via `gh api`; graceful-skip without token) | Yes |
+| Script | Validates | Blocking |
+| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
+| `quality:collect` | Emits `quality-metrics.json` (ESLint warning count, coverage from merged shard report) | Yes (upstream of ratchet) |
+| `quality:ratchet` | Each metric in `quality-baseline.json` has not regressed (ESLint warnings ≤ baseline; coverage ≥ baseline) | Yes |
+| `check:duplication` | Code duplication (jscpd@4) does not exceed baseline in `quality-baseline.json` | Yes |
+| `check:complexity` | File-level cyclomatic complexity does not exceed the cap (core ESLint `complexity` + `max-lines-per-function`) | Yes |
+| `check:cognitive-complexity` | Cognitive complexity ratchet (`eslint-plugin-sonarjs`) — separate ESLint pass; CI runs both merged as the single `check:complexity-ratchets` step | Yes |
+| `check:dead-code` | Unused exports / files ratchet (knip) does not regress vs baseline | Yes |
+| `check:compression-budget` | Compression benchmark budget — per-engine token-savings floors must not regress | Yes |
+| `check:type-coverage` | Percent-typed ratchet (`type-coverage`) does not regress; largely subsumes `typecheck:noimplicit:core` | Yes |
+| `check:codeql-ratchet` | Open CodeQL alert count does not regress (reads via `gh api`; graceful-skip without token) — refresh cadence and manual trigger: see "CodeQL ratchet" below | Yes |
### Job: `quality-extended`
@@ -324,6 +324,36 @@ Commit this file alongside the change that improved the metric. A PR that improv
metric without updating the baseline will be caught by `--require-tighten` (Fase 6A.5,
pending implementation).
+### CodeQL ratchet: refresh cadence and manual trigger
+
+`check:codeql-ratchet` reads **repo state, refreshed on a schedule — not per PR.**
+`gh api repos/diegosouzapw/OmniRoute/code-scanning/default-setup` reports
+`state: configured`, `schedule: weekly`: GitHub's default-setup scan, not a per-push
+analysis. Consequence: after a PR that FIXES alerts merges, the ratchet keeps reading
+the old, higher count until the next scheduled scan runs — so it reports a regression
+on every open PR, including the fixing PR's own follow-ups, until the scan catches up.
+
+**Manual refresh**: `gh workflow run codeql.yml --ref release/vX.Y.Z` re-runs the
+analysis and republishes alerts within minutes. Read `.github/workflows/codeql.yml`
+first — its header explains it is `workflow_dispatch`-only **because it conflicts with
+GitHub's "default setup"** (`CodeQL analyses from advanced configurations cannot be
+processed when the default setup is enabled`). Restoring `push`/`pull_request`/
+`schedule` triggers requires an **owner action first**: Settings → Code security →
+CodeQL: Default → Advanced. Do not add a `schedule:` trigger without that switch — it
+will only produce failing runs.
+
+**Tighten the baseline after the count drops** — `node scripts/check/check-codeql-ratchet.mjs
+--update` writes the new measured count into `quality-baseline.json` →
+`metrics.codeqlAlerts.value`, so the ratchet does not silently permit a regression back
+up to the old ceiling. Worked example (2026-09-02/03): PR #12502 fixed 7 real alerts
+(13 → 6 measured open); PR #12530 tightened the frozen baseline 11 → 6 to match; the
+remaining 6 were then dismissed with per-alert justification down to 0 open.
+
+**Dismissals are the operator's call (Hard Rule #14)** — never dismiss a CodeQL alert
+without recording the technical justification in the dismissal comment: `won't fix` for
+an upstream-protocol requirement, `used in tests` for a test fixture, `false positive`
+for a sanitizer CodeQL cannot see (precedent: `docs/security/ERROR_SANITIZATION.md`).
+
---
## Test Retry Policy (WS5.4, v3.8.49)
diff --git a/docs/compression/COMPRESSION_GUIDE.md b/docs/compression/COMPRESSION_GUIDE.md
index 8f51281fd6..bd2a9e665a 100644
--- a/docs/compression/COMPRESSION_GUIDE.md
+++ b/docs/compression/COMPRESSION_GUIDE.md
@@ -309,7 +309,7 @@ Every compressed request includes stats in the server logs:
| Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped |
| Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped |
| Phase 4 | Output Styles, SLM-tier Ultra, eval harness | ✅ Shipped |
-| Phase 4C | Adaptive context-budget ("dial") — compute engine + API (`contextBudget` on `PUT /api/settings/compression`) | ✅ Shipped (API-configurable; dashboard controls not yet built, #7005) |
+| Phase 4C | Adaptive context-budget ("dial") — compute engine + API (`contextBudget` on `PUT /api/settings/compression`) + dashboard mode/policy controls | ✅ Shipped |
---
diff --git a/docs/diagrams/cli-terminal.svg b/docs/diagrams/cli-terminal.svg
index 139a868898..a16f88351f 100644
--- a/docs/diagrams/cli-terminal.svg
+++ b/docs/diagrams/cli-terminal.svg
@@ -1,4 +1,4 @@
-
+
Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen.
diff --git a/docs/diagrams/comparison-table.svg b/docs/diagrams/comparison-table.svg
index 3bc3895f25..92fe11718c 100644
--- a/docs/diagrams/comparison-table.svg
+++ b/docs/diagrams/comparison-table.svg
@@ -1,4 +1,4 @@
-
+
Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.
diff --git a/docs/diagrams/promise-pillars.svg b/docs/diagrams/promise-pillars.svg
index f32198f62f..41bcdf397c 100644
--- a/docs/diagrams/promise-pillars.svg
+++ b/docs/diagrams/promise-pillars.svg
@@ -1,4 +1,4 @@
-
+
Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.
@@ -21,7 +21,7 @@
- One endpoint. 355 providers. Never stop building — OmniRoute picks the cheapest one that works .
+ One endpoint. 356 providers. Never stop building — OmniRoute picks the cheapest one that works .
@@ -38,7 +38,7 @@
Never hit limits
- Auto-fallback across 355 providers in
+ Auto-fallback across 356 providers in
milliseconds. Quota out? The next provider
takes over while a healthy target remains.
diff --git a/docs/diagrams/readme-hero.svg b/docs/diagrams/readme-hero.svg
index 944176fcc5..7959f33521 100644
--- a/docs/diagrams/readme-hero.svg
+++ b/docs/diagrams/readme-hero.svg
@@ -1,5 +1,4 @@
-
- Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame.
+ Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame.
@@ -28,7 +27,7 @@
Never stop coding.
- Every AI tool → 355 providers — 150+ free — through one endpoint.
+ Every AI tool → 356 providers — 150+ free — through one endpoint.
Claude Code · Codex · Cursor · Cline · Copilot · Antigravity → FREE Claude / GPT / Gemini · auto-fallback
diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md
index 72acc05bfc..ceed44c426 100644
--- a/docs/guides/USER_GUIDE.md
+++ b/docs/guides/USER_GUIDE.md
@@ -122,6 +122,8 @@ Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
## 📖 Provider Setup
+To bulk-add API-key connections from a CSV or JSON file, use **Dashboard → Providers → Import from file**. Columns are positional (`provider,name,apiKey,baseUrl,priority`); `provider` must already exist as a managed provider or a compatible node. See [Import providers from a CSV or JSON file](../providers/CSV-IMPORT.md).
+
### 🔐 Subscription Providers
#### Claude Code (Pro/Max)
diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt
index abd2becf64..9f405eda91 100644
--- a/docs/i18n/ar/llm.txt
+++ b/docs/i18n/ar/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 83aae20b40..74a600ba34 100644
--- a/docs/i18n/az/llm.txt
+++ b/docs/i18n/az/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 19b6eaa3bc..0db6d8a4ce 100644
--- a/docs/i18n/bg/llm.txt
+++ b/docs/i18n/bg/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 ded0a119f8..a7417f642e 100644
--- a/docs/i18n/bn/llm.txt
+++ b/docs/i18n/bn/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 d7d495ce79..c8b5c31922 100644
--- a/docs/i18n/cs/llm.txt
+++ b/docs/i18n/cs/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 fc0f3956f9..3d9663c28f 100644
--- a/docs/i18n/da/llm.txt
+++ b/docs/i18n/da/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 0a3d6f41df..0c8572e4f4 100644
--- a/docs/i18n/de/llm.txt
+++ b/docs/i18n/de/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 a14c3364ca..75a6488e37 100644
--- a/docs/i18n/es/llm.txt
+++ b/docs/i18n/es/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 2637cb93e6..cdcccf2fc8 100644
--- a/docs/i18n/fa/llm.txt
+++ b/docs/i18n/fa/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 dcdbce4e9b..66d62309f4 100644
--- a/docs/i18n/fi/llm.txt
+++ b/docs/i18n/fi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 4ec20698a0..dabd29e87f 100644
--- a/docs/i18n/fr/llm.txt
+++ b/docs/i18n/fr/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 bb96a9ca15..21dad6d8f9 100644
--- a/docs/i18n/gu/llm.txt
+++ b/docs/i18n/gu/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 7b05f7da36..4174e2aa0f 100644
--- a/docs/i18n/he/llm.txt
+++ b/docs/i18n/he/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 d5abf815fd..b08baf5189 100644
--- a/docs/i18n/hi/llm.txt
+++ b/docs/i18n/hi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 242f219733..13109da6fc 100644
--- a/docs/i18n/hu/llm.txt
+++ b/docs/i18n/hu/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 6ea1e4a22c..77df7b4c16 100644
--- a/docs/i18n/id/llm.txt
+++ b/docs/i18n/id/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 d3d4caf7c0..b479c853cd 100644
--- a/docs/i18n/it/llm.txt
+++ b/docs/i18n/it/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 fbd4266e07..a1528c646b 100644
--- a/docs/i18n/ja/llm.txt
+++ b/docs/i18n/ja/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 d72d39d120..4d8508fd12 100644
--- a/docs/i18n/ko/llm.txt
+++ b/docs/i18n/ko/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 d3d06a534b..8ba71d98f5 100644
--- a/docs/i18n/mr/llm.txt
+++ b/docs/i18n/mr/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 fb92e9f704..3903f94cf1 100644
--- a/docs/i18n/ms/llm.txt
+++ b/docs/i18n/ms/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 108465e6ee..8db8eb985b 100644
--- a/docs/i18n/nl/llm.txt
+++ b/docs/i18n/nl/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 92ef34cc68..e635301c86 100644
--- a/docs/i18n/no/llm.txt
+++ b/docs/i18n/no/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 ed4c534f3b..b760a56857 100644
--- a/docs/i18n/phi/llm.txt
+++ b/docs/i18n/phi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 9c2d022df6..e683cdb76c 100644
--- a/docs/i18n/pl/llm.txt
+++ b/docs/i18n/pl/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 1d6c34ce26..49a62961e7 100644
--- a/docs/i18n/pt-BR/llm.txt
+++ b/docs/i18n/pt-BR/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 5be880273e..9362022f86 100644
--- a/docs/i18n/pt/llm.txt
+++ b/docs/i18n/pt/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 a9006002d2..fd21c2042b 100644
--- a/docs/i18n/ro/llm.txt
+++ b/docs/i18n/ro/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 fb3bd9901f..d68314887e 100644
--- a/docs/i18n/ru/llm.txt
+++ b/docs/i18n/ru/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 30a439beb7..666d1a570e 100644
--- a/docs/i18n/sk/llm.txt
+++ b/docs/i18n/sk/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 a3e29a0308..74238247ae 100644
--- a/docs/i18n/sv/llm.txt
+++ b/docs/i18n/sv/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 3e50fc5415..5f129a8263 100644
--- a/docs/i18n/sw/llm.txt
+++ b/docs/i18n/sw/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 26cc42048a..d42fa346a0 100644
--- a/docs/i18n/ta/llm.txt
+++ b/docs/i18n/ta/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 95b13b644a..6ca040018f 100644
--- a/docs/i18n/te/llm.txt
+++ b/docs/i18n/te/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 0c144efb4f..9884ab3b21 100644
--- a/docs/i18n/th/llm.txt
+++ b/docs/i18n/th/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 9c5ce3e607..77b9eadb1b 100644
--- a/docs/i18n/tr/llm.txt
+++ b/docs/i18n/tr/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 407ee0fdfa..3a7bc01f36 100644
--- a/docs/i18n/uk-UA/llm.txt
+++ b/docs/i18n/uk-UA/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 b80c1a22cc..1cdd0e6bb5 100644
--- a/docs/i18n/ur/llm.txt
+++ b/docs/i18n/ur/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 8aaea70f4e..3e28435b0a 100644
--- a/docs/i18n/vi/llm.txt
+++ b/docs/i18n/vi/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 2a9812d5f2..d11163a83a 100644
--- a/docs/i18n/zh-CN/llm.txt
+++ b/docs/i18n/zh-CN/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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 2caf3c753c..56256a53da 100644
--- a/docs/i18n/zh-TW/llm.txt
+++ b/docs/i18n/zh-TW/llm.txt
@@ -4,7 +4,7 @@
---
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -281,7 +281,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -393,7 +393,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -437,7 +437,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 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/openapi.yaml b/docs/openapi.yaml
index 2f83d88080..16a39eef8d 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -2297,6 +2297,7 @@ paths:
post:
tags: [Providers]
summary: Auto-detect and import the local Antigravity CLI (agy) login from disk
+ x-always-protected: true
responses:
"200":
description: Created or updated provider connection
@@ -4050,12 +4051,14 @@ paths:
get:
tags: [CLI Tools]
summary: Get Codex profiles
+ x-always-protected: true
responses:
"200":
description: Codex profile list
post:
tags: [CLI Tools]
summary: Create Codex profile
+ x-always-protected: true
requestBody:
required: true
content:
@@ -4068,6 +4071,7 @@ paths:
put:
tags: [CLI Tools]
summary: Update Codex profile
+ x-always-protected: true
requestBody:
required: true
content:
@@ -4080,6 +4084,7 @@ paths:
delete:
tags: [CLI Tools]
summary: Delete Codex profile
+ x-always-protected: true
responses:
"200":
description: Profile deleted
@@ -9751,6 +9756,7 @@ paths:
tags:
- Logs
summary: "GET logs › export"
+ x-always-protected: true
responses:
"200":
description: OK
@@ -10230,6 +10236,7 @@ paths:
tags:
- Providers
summary: "POST providers › › claude auth › apply local"
+ x-always-protected: true
responses:
"200":
description: OK
@@ -10238,6 +10245,7 @@ paths:
tags:
- Providers
summary: "POST providers › › claude auth › export"
+ x-always-protected: true
responses:
"200":
description: OK
@@ -10246,6 +10254,7 @@ paths:
tags:
- Providers
summary: "POST providers › › codex auth › apply local"
+ x-always-protected: true
responses:
"200":
description: OK
@@ -10254,6 +10263,7 @@ paths:
tags:
- Providers
summary: "POST providers › › codex auth › export"
+ x-always-protected: true
responses:
"200":
description: OK
diff --git a/docs/providers/AGENTROUTER.md b/docs/providers/AGENTROUTER.md
index aa28834c75..ea375f43c9 100644
--- a/docs/providers/AGENTROUTER.md
+++ b/docs/providers/AGENTROUTER.md
@@ -130,7 +130,7 @@ request (see `open-sse/services/claudeCodeCompatible.ts`):
| Header | Value |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `Authorization` | `Bearer ` |
-| `User-Agent` | `claude-cli/2.1.219 (external, sdk-cli)` |
+| `User-Agent` | `claude-cli/2.1.258 (external, sdk-cli)` |
| `anthropic-version` | `2023-06-01` |
| `anthropic-beta` | `claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24` |
| Per-connection redact-thinking beta toggle | Adds `redact-thinking-2026-02-12` for upstreams that specifically require redacted thinking streams |
diff --git a/docs/providers/CSV-IMPORT.md b/docs/providers/CSV-IMPORT.md
new file mode 100644
index 0000000000..4f701a16a2
--- /dev/null
+++ b/docs/providers/CSV-IMPORT.md
@@ -0,0 +1,43 @@
+---
+title: "Import providers from a CSV or JSON file"
+---
+
+# Import providers from a CSV or JSON file
+
+Dashboard → Providers → **Import from file** creates API-key connections from a CSV or JSON list. Each row can target a different provider. Partial failure is the contract: valid rows still import when others fail, and the modal lists why the failed rows were rejected.
+
+This import does **not** create new OpenAI/Anthropic-compatible endpoint nodes. Create those first (Dashboard → Providers → Add OpenAI-Compatible, or `omniroute nodes add`), then import rows whose `provider` column is that node's id. A per-row `baseUrl` can still override the node's URL.
+
+## CSV (positional)
+
+Column names are cosmetic. The parser splits each row and destructures by index:
+
+| Index | Field | Required | Notes |
+| ----- | ----- | -------- | ----- |
+| 0 | `provider` | yes | Existing managed provider id (`openai`, `anthropic`, …) **or** an already-registered OpenAI/Anthropic-compatible **node** id |
+| 1 | `name` | yes | Connection display name |
+| 2 | `apiKey` | yes | API key |
+| 3 | `baseUrl` | no | Per-row URL override |
+| 4 | `priority` | no | Integer 1–100 |
+
+A first line whose first column is the literal word `provider` (any case) is skipped as a header. Blank lines and `#` comments are skipped.
+
+Download a starter file from the import modal (**Download CSV template**). Example:
+
+```csv
+# OmniRoute provider import (positional columns)
+provider,name,apiKey,baseUrl,priority
+openai,Prod OpenAI,sk-your-openai-key,,1
+```
+
+A made-up id such as `openai-compatible-chat-001` is not a node. The API returns `Unknown or unsupported provider` for that row; the modal shows it next to the row name.
+
+## JSON
+
+A JSON array of objects with the same fields (`provider`, `name`, `apiKey`, `baseUrl?`, `priority?`). Unlike CSV, JSON keys are named.
+
+```json
+[
+ { "provider": "openai", "name": "Prod OpenAI", "apiKey": "sk-your-openai-key", "priority": 1 }
+]
+```
diff --git a/docs/providers/meta.json b/docs/providers/meta.json
index fa6485dd57..b5eca33685 100644
--- a/docs/providers/meta.json
+++ b/docs/providers/meta.json
@@ -8,6 +8,7 @@
"AGENTROUTER",
"ZED-DOCKER",
"CURSOR-DOCKER",
- "CURSOR-API-KEY-AND-CLI"
+ "CURSOR-API-KEY-AND-CLI",
+ "CSV-IMPORT"
]
}
diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md
index ace16cf6c9..056a37beb9 100644
--- a/docs/reference/ENVIRONMENT.md
+++ b/docs/reference/ENVIRONMENT.md
@@ -622,7 +622,7 @@ process.env[`${PROVIDER_ID}_USER_AGENT`]
| Variable | Default Value | When to Update |
| -------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `CLAUDE_USER_AGENT` | `claude-cli/2.1.219 (external, cli)` | When Anthropic releases a new CLI version |
+| `CLAUDE_USER_AGENT` | `claude-cli/2.1.258 (external, cli)` | When Anthropic releases a new CLI version |
| `CLAUDE_DISABLE_TOOL_NAME_CLOAK` | `false` | `executors/base.ts` + `executors/cliproxyapi.ts` | Set to `1`/`true` to forward third-party harness tool names verbatim to Anthropic on both Anthropic-bound paths (native OAuth and CLIProxyAPI). By default the executor deterministically aliases non-Claude-Code tool names (Claude Code canonical mapping where one exists, otherwise PascalCase) and reverses them on the response via `_toolNameMap`, so harnesses with snake_case tools are not refused as fingerprinted third-party clients. Debugging only. |
| `CODEX_USER_AGENT` | `codex-cli/0.142.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI |
| `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string |
@@ -776,6 +776,9 @@ REQUEST_TIMEOUT_MS (global override)
| `OMNIROUTE_NOTION_TLS_TIMEOUT_MS` | `30000` | Native wreq-js request timeout (`notionTlsClient.ts`); `notion-web` raises it per request to `180000` for long generations. |
| `OMNIROUTE_NOTION_TLS_GRACE_MS` | `10000` | Absolute JS hard-deadline grace added on top of the native timeout. |
| `OMNIROUTE_BROWSER_POOL` | `on` | Shared Playwright browser pool for browser-backed web-cookie chat (`browserPool.ts`); set `off` to disable. |
+| `OBSCURA_BIN` | `auto-detect` | Path to the `obscura` binary used as the primary engine by the browser pool and Cloudflare Playground executor (`open-sse/services/obscura.ts`); auto-detected from the system PATH when unset. |
+| `OBSCURA_CDP_ENDPOINT` | _(unset)_ | Point at an already-running Obscura (`http://host:port`) instead of spawning one; the module does not own that process (`open-sse/services/obscura.ts`). |
+| `OBSCURA_PORT` | `random free port` | Explicit port for the spawned `obscura serve`; a free port is chosen automatically when unset (`open-sse/services/obscura.ts`). |
| `WEB_COOKIE_USE_BROWSER` | `0` | Opt a web-cookie chat request into the browser-backed path (`browserBackedChat.ts`); `1` to enable. |
| `KIMI_WEB_BASE_URL` | `https://www.kimi.ai` | Base URL for the Kimi Web (international kimi.ai Connect-RPC) executor (`kimi-web.ts`); override only for mirror/proxy endpoints. |
| `KIMI_WEB_CHAT_URL` | `/apiv2/kimi.gateway.chat.v1.ChatService/Chat` | Full chat endpoint for the Kimi Web executor (`kimi-web.ts`). |
diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md
index c577652c8c..6ddd59d202 100644
--- a/docs/reference/PROVIDER_REFERENCE.md
+++ b/docs/reference/PROVIDER_REFERENCE.md
@@ -1,16 +1,16 @@
---
title: "Provider Reference"
version: 3.8.51
-lastUpdated: 2026-09-02
+lastUpdated: 2026-09-03
---
# Provider Reference
> **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand.
> Regenerate with: `npm run gen:provider-reference`
-> **Last generated:** 2026-09-02
+> **Last generated:** 2026-09-03
-Total providers: **355**. See category breakdown below.
+Total providers: **356**. See category breakdown below.
## Categories
@@ -118,7 +118,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — |
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — |
-## API Key Providers (paid / paid-with-free-credits) (237)
+## API Key Providers (paid / paid-with-free-credits) (238)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -310,6 +310,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `sarvam` | `sarvam` | Sarvam AI | API key | [link](https://docs.sarvam.ai) | ₹1,000 in free signup credits — never expire |
| `scaleway` | `scw` | Scaleway AI | API key | [link](https://www.scaleway.com/en/docs/ai-data/generative-apis/) | 1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B |
| `sealion` | `sealion` | SEA-LION | API key | [link](https://sea-lion.ai) | Sign in at sea-lion.ai with Google (no card, no region wall), create an API key, then paste it here. |
+| `seekai` | `ska` | SeekAi | API key, aggregator | [link](https://seekai.cc) | Create an API key at https://seekai.cc, then paste it here as a Bearer token. |
| `segmind` | `segmind` | Segmind | API key, image, video | [link](https://segmind.com) | Use your Segmind API key in the x-api-key header. OmniRoute targets https://api.segmind.com/v1/ and returns the generated image/video bytes directly. |
| `sensenova` | `sensenova` | SenseNova | API key | [link](https://platform.sensenova.cn) | Get API key at platform.sensenova.cn |
| `siliconflow` | `siliconflow` | SiliconFlow | API key | [link](https://cloud.siliconflow.com) | $1 free credits plus currently listed $0 models after identity verification; availability and limits may change |
diff --git a/docs/reference/REMOVED_PROVIDERS.md b/docs/reference/REMOVED_PROVIDERS.md
index e03283bd43..24c0aab7d4 100644
--- a/docs/reference/REMOVED_PROVIDERS.md
+++ b/docs/reference/REMOVED_PROVIDERS.md
@@ -1,3 +1,9 @@
+---
+title: "Removed Providers"
+version: 3.8.51
+lastUpdated: 2026-09-03
+---
+
# Providers removed at their operator's request
Some services were integrated into OmniRoute and later removed because the people who run
diff --git a/docs/security/STEALTH_GUIDE.md b/docs/security/STEALTH_GUIDE.md
index 456a558b0c..767eb90a0b 100644
--- a/docs/security/STEALTH_GUIDE.md
+++ b/docs/security/STEALTH_GUIDE.md
@@ -117,8 +117,8 @@ Applied to: `system` blocks, all `messages[].content`, and `tools[].description`
For third-party Anthropic relays that only accept "real Claude Code" traffic:
-- `CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.220 (external, sdk-cli)"`
-- `CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.94.0"`
+- `CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.258 (external, sdk-cli)"`
+- `CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.112.1"`
- `CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v26.3.0"`
- `anthropic-beta = "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24"` by default
- The per-connection "Enable redact-thinking beta" toggle adds `redact-thinking-2026-02-12` when a CC Compatible upstream specifically requires redacted thinking streams
@@ -241,7 +241,7 @@ All MITM endpoints require management auth (`requireCliToolsAuth`). The sudo pas
| Variable | Default |
| ------------------------ | --------------------------------------------------------------- |
-| `CLAUDE_USER_AGENT` | `claude-cli/2.1.220 (external, cli)` |
+| `CLAUDE_USER_AGENT` | `claude-cli/2.1.258 (external, cli)` |
| `CODEX_USER_AGENT` | `codex-cli/0.149.0 (Windows 10.0.26200; x64)` |
| `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.54.0` |
| `ANTIGRAVITY_USER_AGENT` | `antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0` |
diff --git a/electron/lib/windowLifecycle.js b/electron/lib/windowLifecycle.js
index 8a75a4a18d..cea524ab37 100644
--- a/electron/lib/windowLifecycle.js
+++ b/electron/lib/windowLifecycle.js
@@ -1,11 +1,11 @@
/** Pure helpers for deciding and driving the Electron dashboard window lifecycle. */
-function shouldStartHidden({ argv = [], loginItemSettings = {} } = {}) {
- return (
- argv.includes("--hidden") ||
- argv.includes("--minimized") ||
- loginItemSettings.wasOpenedAsHidden === true
- );
+// Electron 44 removed `openAsHidden`/`wasOpenedAsHidden` from
+// `app.set/getLoginItemSettings()` (they only ever worked on macOS 12 and below, which
+// Electron 44 no longer supports). The hidden-autostart contract is now carried solely by
+// the `--hidden` argument registered with the login item.
+function shouldStartHidden({ argv = [] } = {}) {
+ return argv.includes("--hidden") || argv.includes("--minimized");
}
function showOrCreateWindow({ appReady, getWindow, createWindow }) {
diff --git a/electron/main.js b/electron/main.js
index 19f232226b..2010fe5683 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -1113,7 +1113,6 @@ function setupIpcHandlers() {
try {
app.setLoginItemSettings({
openAtLogin: true,
- openAsHidden: true,
args: ["--hidden"],
});
return true;
@@ -1153,7 +1152,6 @@ app.whenReady().then(async () => {
!isHeadless &&
shouldStartHidden({
argv: process.argv,
- loginItemSettings: app.getLoginItemSettings(),
});
keepAliveWithoutWindows = startHidden;
diff --git a/electron/package-lock.json b/electron/package-lock.json
index 1aeabc7edc..8eb9f2634a 100644
--- a/electron/package-lock.json
+++ b/electron/package-lock.json
@@ -12,7 +12,7 @@
"electron-updater": "^6.8.9"
},
"devDependencies": {
- "electron": "^43.4.1",
+ "electron": "^44.0.0",
"electron-builder": "^26.15.3"
},
"engines": {
@@ -1367,9 +1367,9 @@
}
},
"node_modules/electron": {
- "version": "43.4.1",
- "resolved": "https://registry.npmjs.org/electron/-/electron-43.4.1.tgz",
- "integrity": "sha512-5b+EuiwkgG5iRcsEL34rimgRpkYp15SsfZOa0pC5kXs0Tb82TH4n95rpQzTZa7yRCbA7tm0WoEbuBL6NaAhAcA==",
+ "version": "44.0.0",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-44.0.0.tgz",
+ "integrity": "sha512-FkTqPrFPZYljdPI5b7KORGsJTd6FgUQDefl5MrU3Xz9R87pAj9JLreIjDqcRN8hJIkFHIou0o8kKzvcpT9qiRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1575,9 +1575,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
- "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"dev": true,
"funding": [
{
diff --git a/electron/package.json b/electron/package.json
index d4fd2bd1b2..1ed7410a4d 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -28,7 +28,7 @@
"electron-updater": "^6.8.9"
},
"devDependencies": {
- "electron": "^43.4.1",
+ "electron": "^44.0.0",
"electron-builder": "^26.15.3"
},
"overrides": {
diff --git a/llm.txt b/llm.txt
index 9789ad5c22..3748b08432 100644
--- a/llm.txt
+++ b/llm.txt
@@ -1,6 +1,6 @@
# OmniRoute
-> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 355 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
+> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 356 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (110 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.22.2 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
-- **Database:** SQLite via better-sqlite3 (local, zero-config, 168 migrations)
+- **Database:** SQLite via better-sqlite3 (local, zero-config, 169 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/ # 168 versioned SQL migration files
+│ │ │ └── migrations/ # 169 versioned SQL migration files
│ │ ├── evals/ # Eval runner and scheduler
│ │ ├── memory/ # Persistent conversational memory
│ │ │ ├── extraction.ts # Memory extraction from conversations
@@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
-- **355 AI providers** with automatic format translation
+- **356 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **19 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -389,7 +389,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
8. **ProviderIcon component:** Unified icon system using `@lobehub/icons` (130+ SVG) with PNG fallback and generic icon fallback chain. Used on providers, dashboard, and agents pages.
-9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 168 SQL migrations.
+9. **DB architecture:** `localDb.ts` is a re-export layer only — real logic lives in 122 `src/lib/db/` modules with 169 SQL migrations.
10. **Upstream headers:** Custom headers merged in executors after default auth; same header name replaces executor value. Forbidden header names in `src/shared/constants/upstreamHeaders.ts`.
@@ -433,7 +433,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
-5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 168 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
+5. **Database layer:** Operations go through `src/lib/db/` modules (122 domain-specific files, 169 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
diff --git a/open-sse/config/claudeCodeCompatibleIdentity.ts b/open-sse/config/claudeCodeCompatibleIdentity.ts
index 31b7997f2a..b614eea324 100644
--- a/open-sse/config/claudeCodeCompatibleIdentity.ts
+++ b/open-sse/config/claudeCodeCompatibleIdentity.ts
@@ -9,15 +9,19 @@ export const CLAUDE_CODE_COMPATIBLE_VERSION = CLAUDE_CODE_CLIENT_VERSION;
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = getClaudeCodeUserAgent("sdk-cli");
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = CLAUDE_CODE_SDK_PACKAGE_VERSION;
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = CLAUDE_CODE_RUNTIME_VERSION;
-const CONTEXT_1M_NATIVE_MODELS = ["claude-opus-5"];
+const CONTEXT_1M_NATIVE_MODELS = ["claude-fable-5-1", "claude-opus-5"];
export function modelHasNativeContext1m(model: string | null | undefined): boolean {
const normalizedModel = String(model || "")
.trim()
.toLowerCase()
+ .replace(/^.*?(?=claude-)/, "")
.replace(/-\d{8}$/, "");
return CONTEXT_1M_NATIVE_MODELS.some(
- (supported) => normalizedModel === supported || normalizedModel.startsWith(`${supported}-`)
+ (supported) =>
+ normalizedModel === supported ||
+ (normalizedModel.startsWith(`${supported}-`) &&
+ !/^\d/.test(normalizedModel.slice(supported.length + 1)))
);
}
diff --git a/open-sse/config/context1m.ts b/open-sse/config/context1m.ts
index dab5c405b1..134df22bf8 100644
--- a/open-sse/config/context1m.ts
+++ b/open-sse/config/context1m.ts
@@ -34,6 +34,9 @@ export function modelSupportsContext1mBeta(model: string | null | undefined): bo
.replace(/-\d{8}$/, "");
return CONTEXT_1M_SUPPORTED_MODELS.some(
- (supported) => normalizedModel === supported || normalizedModel.startsWith(`${supported}-`)
+ (supported) =>
+ normalizedModel === supported ||
+ (normalizedModel.startsWith(`${supported}-`) &&
+ !/^\d/.test(normalizedModel.slice(supported.length + 1)))
);
-}
\ No newline at end of file
+}
diff --git a/open-sse/config/embeddingRegistry.ts b/open-sse/config/embeddingRegistry.ts
index c4ab2dc2fd..562d2f63c8 100644
--- a/open-sse/config/embeddingRegistry.ts
+++ b/open-sse/config/embeddingRegistry.ts
@@ -10,6 +10,7 @@
export type EmbeddingModality = "text" | "image" | "audio" | "video" | "document";
export type StructuredEmbeddingProtocol = "jina-v1" | "gemini-embed-content";
+export type SingleTextEmbeddingProtocol = "clova-v2";
export interface EmbeddingModel {
id: string;
@@ -34,6 +35,13 @@ export interface EmbeddingProvider {
models: EmbeddingModel[];
/** Provider-native serializer required for canonical structured input. */
structuredInputProtocol?: StructuredEmbeddingProtocol;
+ /**
+ * Set when the endpoint embeds exactly ONE text per request (`{"text": …}` →
+ * one vector) instead of accepting OpenAI's `input` array. A batched
+ * `/v1/embeddings` call is then fanned out into N sequential upstream calls and
+ * merged back into a single OpenAI list response.
+ */
+ singleTextProtocol?: SingleTextEmbeddingProtocol;
}
export interface EmbeddingProviderNodeRow {
@@ -297,6 +305,18 @@ export const EMBEDDING_PROVIDERS: Record = {
],
},
+ // Naver CLOVA Studio — embedding v2. The endpoint takes a single `{"text": …}`
+ // body and returns `{status, result:{embedding:[…1024 floats], inputTokens}}`,
+ // with no batch array and no `usage` object, hence `singleTextProtocol`.
+ "clova-studio": {
+ id: "clova-studio",
+ baseUrl: "https://clovastudio.stream.ntruss.com/v1/api-tools/embedding/v2",
+ authType: "apikey",
+ authHeader: "bearer",
+ singleTextProtocol: "clova-v2",
+ models: [{ id: "clova-embedding-v2", name: "CLOVA Embedding v2", dimensions: 1024 }],
+ },
+
"jina-ai": {
id: "jina-ai",
structuredInputProtocol: "jina-v1",
@@ -471,6 +491,62 @@ export function getEmbeddingProvider(providerId: string): EmbeddingProvider | nu
return EMBEDDING_PROVIDERS[resolveEmbeddingProviderId(providerId)] || null;
}
+function findDynamicEmbeddingProvider(
+ modelStr: string,
+ dynamicProviders: EmbeddingProvider[] | undefined
+): { provider: string; model: string } | null {
+ const match = dynamicProviders?.find((provider) => modelStr.startsWith(`${provider.id}/`));
+ return match ? { provider: match.id, model: modelStr.slice(match.id.length + 1) } : null;
+}
+
+function parsePrefixedEmbeddingModel(
+ modelStr: string,
+ slashIdx: number,
+ dynamicProviders: EmbeddingProvider[] | undefined
+): { provider: string; model: string } {
+ const rawProvider = modelStr.slice(0, slashIdx);
+ const dynamicExact = dynamicProviders?.find((provider) => provider.id === rawProvider);
+ if (dynamicExact) {
+ return { provider: rawProvider, model: modelStr.slice(slashIdx + 1) };
+ }
+
+ const resolvedProvider = resolveEmbeddingProviderId(rawProvider);
+ if (EMBEDDING_PROVIDERS[resolvedProvider]) {
+ return {
+ provider: resolvedProvider,
+ model: normalizeProviderScopedModelId(resolvedProvider, modelStr.slice(slashIdx + 1)),
+ };
+ }
+
+ const hardcodedProvider = Object.keys(EMBEDDING_PROVIDERS).find((providerId) =>
+ modelStr.startsWith(`${providerId}/`)
+ );
+ if (hardcodedProvider) {
+ return {
+ provider: hardcodedProvider,
+ model: normalizeProviderScopedModelId(
+ hardcodedProvider,
+ modelStr.slice(hardcodedProvider.length + 1)
+ ),
+ };
+ }
+
+ return (
+ findDynamicEmbeddingProvider(modelStr, dynamicProviders) ?? {
+ provider: rawProvider,
+ model: modelStr.slice(slashIdx + 1),
+ }
+ );
+}
+
+function findEmbeddingModelProvider(modelStr: string): string | null {
+ return (
+ Object.entries(EMBEDDING_PROVIDERS).find(([, config]) =>
+ config.models.some((model) => model.id === modelStr)
+ )?.[0] ?? null
+ );
+}
+
/**
* Derive an OpenAI-compatible embeddings config for a chat provider that has NO
* curated EMBEDDING_PROVIDERS entry. Works for any registry provider whose base
@@ -517,59 +593,11 @@ export function parseEmbeddingModel(
// Check for "provider/model" format
const slashIdx = modelStr.indexOf("/");
if (slashIdx > 0) {
- const rawProvider = modelStr.slice(0, slashIdx);
-
- // A configured provider_node whose prefix exactly equals the requested
- // provider segment always wins — even when that segment is also an alias
- // of a curated provider (a local node must not be hijacked by a registry
- // alias). Same exact-match precedence documented for
- // EMBEDDING_MODEL_ALIASES above.
- const dynamicExact =
- dynamicProviders && dynamicProviders.find((dp) => dp.id === rawProvider);
- if (dynamicExact) {
- return { provider: rawProvider, model: modelStr.slice(slashIdx + 1) };
- }
-
- const resolvedProvider = resolveEmbeddingProviderId(rawProvider);
-
- if (EMBEDDING_PROVIDERS[resolvedProvider]) {
- return {
- provider: resolvedProvider,
- model: normalizeProviderScopedModelId(resolvedProvider, modelStr.slice(slashIdx + 1)),
- };
- }
-
- // Phase 1: Try each hardcoded provider prefix
- for (const [providerId] of Object.entries(EMBEDDING_PROVIDERS)) {
- if (modelStr.startsWith(providerId + "/")) {
- return {
- provider: providerId,
- model: normalizeProviderScopedModelId(providerId, modelStr.slice(providerId.length + 1)),
- };
- }
- }
- // Phase 2: Try dynamic provider_nodes prefix
- if (dynamicProviders) {
- for (const dp of dynamicProviders) {
- if (modelStr.startsWith(dp.id + "/")) {
- return { provider: dp.id, model: modelStr.slice(dp.id.length + 1) };
- }
- }
- }
- // Phase 3: Fallback — first segment is provider
- const provider = modelStr.slice(0, slashIdx);
- const model = modelStr.slice(slashIdx + 1);
- return { provider, model };
+ return parsePrefixedEmbeddingModel(modelStr, slashIdx, dynamicProviders);
}
// No provider prefix — search hardcoded providers for the model
- for (const [providerId, config] of Object.entries(EMBEDDING_PROVIDERS)) {
- if (config.models.some((m) => m.id === modelStr)) {
- return { provider: providerId, model: modelStr };
- }
- }
-
- return { provider: null, model: modelStr };
+ return { provider: findEmbeddingModelProvider(modelStr), model: modelStr };
}
/**
diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts
index 64e6a9af7d..dc45a451e9 100644
--- a/open-sse/config/freeModelCatalog.data.ts
+++ b/open-sse/config/freeModelCatalog.data.ts
@@ -301,18 +301,9 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "nscale", modelId: "openai/gpt-oss-20b", displayName: "openai/gpt-oss-20b", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "nscale", tos: "caution" },
{ provider: "nscale", modelId: "meta-llama/Llama-4-Scout-17B-16E-Instruct", displayName: "meta-llama/Llama-4-Scout-17B-16E-Instruct", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "nscale", tos: "caution" },
{ provider: "nscale", modelId: "meta-llama/Llama-3.3-70B-Instruct", displayName: "meta-llama/Llama-3.3-70B-Instruct", monthlyTokens: 0, creditTokens: 5000000, freeType: "one-time-initial", poolKey: "nscale", tos: "caution" },
- { provider: "nvidia", modelId: "z-ai/glm-5.2", displayName: "GLM 5.2", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "minimaxai/minimax-m2.7", displayName: "MiniMax M2.7", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
{ provider: "nvidia", modelId: "google/gemma-4-31b-it", displayName: "Gemma 4 31B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "mistralai/mistral-small-4-119b-2603", displayName: "Mistral Small 4 2603", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "mistralai/mistral-large-3-675b-instruct-2512", displayName: "Mistral Large 3 675B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "mistralai/devstral-2-123b-instruct-2512", displayName: "Devstral 2 123B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "qwen/qwen3.5-397b-a17b", displayName: "Qwen3.5-397B-A17B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "qwen/qwen3.5-122b-a10b", displayName: "Qwen3.5-122B-A10B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "stepfun-ai/step-3.5-flash", displayName: "Step 3.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "openai/gpt-oss-120b", displayName: "GPT OSS 120B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
- { provider: "nvidia", modelId: "openai/gpt-oss-20b", displayName: "GPT OSS 20B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
{ provider: "nvidia", modelId: "nvidia/nemotron-3-super-120b-a12b", displayName: "Nemotron 3 Super 120B A12B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
+ { provider: "nvidia", modelId: "openai/gpt-oss-120b", displayName: "GPT OSS 120B", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "nvidia", tos: "caution" },
// evidence: public-page https://ollama.com/pricing (2026-09-02) — Free plan: "Starter usage credits
// included · Includes access to starter models · Add credits to unlock all models"; docs.ollama.com/cloud:
// "usage resets monthly". No token figure and no named starter-model list ⇒ recurring-uncapped.
diff --git a/open-sse/config/nvidiaHostedModels.snapshot.json b/open-sse/config/nvidiaHostedModels.snapshot.json
index 60d2f2e76d..fa94a6a4a7 100644
--- a/open-sse/config/nvidiaHostedModels.snapshot.json
+++ b/open-sse/config/nvidiaHostedModels.snapshot.json
@@ -1,16 +1,14 @@
[
- "google/gemma-4-31b-it",
- "minimaxai/minimax-m2.7",
- "mistralai/devstral-2-123b-instruct-2512",
- "mistralai/mistral-large-3-675b-instruct-2512",
- "mistralai/mistral-small-4-119b-2603",
- "nvidia/nemotron-3-super-120b-a12b",
- "openai/gpt-oss-120b",
- "openai/gpt-oss-20b",
+ "moonshotai/kimi-k3",
+ "deepseek-ai/deepseek-v4-pro-0813",
+ "deepseek-ai/deepseek-v4-flash-0731",
+ "meta/muse-glimmer-30b",
"poolside/laguna-xs-2.1",
- "qwen/qwen3.5-122b-a10b",
- "qwen/qwen3.5-397b-a17b",
- "stepfun-ai/step-3.5-flash",
- "thinkingmachines/inkling",
- "z-ai/glm-5.2"
+ "google/gemma-4-31b-it",
+ "google/diffusiongemma-26b-a4b-it",
+ "nvidia/nemotron-3-ultra-550b-a55b",
+ "nvidia/nemotron-3-super-120b-a12b",
+ "nvidia/nemotron-3.5-lightning-30b-a3b",
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
+ "openai/gpt-oss-120b"
]
diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts
index 1e6b542d13..e6ee28c65f 100644
--- a/open-sse/config/providers/index.ts
+++ b/open-sse/config/providers/index.ts
@@ -270,6 +270,7 @@ import { voidAiProvider } from "./registry/void-ai/index.ts";
import { helixmindProvider } from "./registry/helixmind/index.ts";
import { tabitokenProvider } from "./registry/tabitoken/index.ts";
import { logfareProvider } from "./registry/logfare/index.ts";
+import { seekaiProvider } from "./registry/seekai/index.ts";
export const REGISTRY: Record = {
aimlapi: aimlapiProvider,
@@ -544,4 +545,5 @@ export const REGISTRY: Record = {
helixmind: helixmindProvider,
tabitoken: tabitokenProvider,
logfare: logfareProvider,
+ seekai: seekaiProvider,
};
diff --git a/open-sse/config/providers/registry/anthropic/index.ts b/open-sse/config/providers/registry/anthropic/index.ts
index 2a3726276d..2845296b15 100644
--- a/open-sse/config/providers/registry/anthropic/index.ts
+++ b/open-sse/config/providers/registry/anthropic/index.ts
@@ -16,6 +16,17 @@ export const anthropicProvider: RegistryEntry = {
"Anthropic-Beta": ANTHROPIC_BETA_API_KEY,
},
models: [
+ {
+ id: "claude-fable-5-1",
+ name: "Claude Fable 5.1",
+ contextLength: 1000000,
+ maxOutputTokens: 128000,
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
+ supportsXHighEffort: true,
+ supportsVision: true,
+ unsupportedParams: ["temperature", "top_p", "top_k"],
+ },
{
id: "claude-fable-5",
name: "Claude Fable 5",
diff --git a/open-sse/config/providers/registry/bedrock/index.ts b/open-sse/config/providers/registry/bedrock/index.ts
index bb9273fda0..ce70091a27 100644
--- a/open-sse/config/providers/registry/bedrock/index.ts
+++ b/open-sse/config/providers/registry/bedrock/index.ts
@@ -9,6 +9,17 @@ export const bedrockProvider: RegistryEntry = {
authHeader: "bearer",
defaultContextLength: 200000,
models: [
+ {
+ id: "anthropic.claude-fable-5-1",
+ name: "Claude Fable 5.1 (Bedrock)",
+ toolCalling: true,
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
+ supportsXHighEffort: true,
+ supportsVision: true,
+ contextLength: 1000000,
+ maxOutputTokens: 128000,
+ },
{
id: "anthropic.claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (Bedrock)",
diff --git a/open-sse/config/providers/registry/claude/index.ts b/open-sse/config/providers/registry/claude/index.ts
index 481c831c2f..ba3129678b 100644
--- a/open-sse/config/providers/registry/claude/index.ts
+++ b/open-sse/config/providers/registry/claude/index.ts
@@ -28,6 +28,17 @@ export const claudeProvider: RegistryEntry = {
tokenUrl: "https://api.anthropic.com/v1/oauth/token",
},
models: [
+ {
+ id: "claude-fable-5-1",
+ name: "Claude Fable 5.1",
+ contextLength: 1000000,
+ maxOutputTokens: 128000,
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
+ supportsXHighEffort: true,
+ supportsVision: true,
+ unsupportedParams: ["temperature", "top_p", "top_k"],
+ },
{
id: "claude-fable-5",
name: "Claude Fable 5",
diff --git a/open-sse/config/providers/registry/claude/web/index.ts b/open-sse/config/providers/registry/claude/web/index.ts
index c701ff9aa3..07c4872362 100644
--- a/open-sse/config/providers/registry/claude/web/index.ts
+++ b/open-sse/config/providers/registry/claude/web/index.ts
@@ -9,6 +9,17 @@ export const claude_webProvider: RegistryEntry = {
authType: "apikey",
authHeader: "cookie",
models: [
+ {
+ id: "claude-fable-5-1",
+ name: "Claude Fable 5.1 (web)",
+ toolCalling: false,
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
+ supportsXHighEffort: true,
+ supportsVision: true,
+ contextLength: 1000000,
+ maxOutputTokens: 128000,
+ },
{ id: "claude-fable-5", name: "Claude Fable 5 (web)", toolCalling: false },
{
id: "claude-opus-5",
diff --git a/open-sse/config/providers/registry/clova-studio/index.ts b/open-sse/config/providers/registry/clova-studio/index.ts
index 8344ebe911..dd371a0336 100644
--- a/open-sse/config/providers/registry/clova-studio/index.ts
+++ b/open-sse/config/providers/registry/clova-studio/index.ts
@@ -1,17 +1,75 @@
import type { RegistryEntry } from "../../shared.ts";
+/**
+ * Naver CLOVA Studio — Chat Completions **v3** (native API).
+ *
+ * Previously this entry pointed at Naver's OpenAI-compatibility shim
+ * (`/v1/openai/chat/completions`), which meant `format: "openai"` and a
+ * pass-through `DefaultExecutor`. The v3 API is Naver's own wire format, so the
+ * entry now uses `format: "clova"` and the translator pair
+ * (`openai-to-clova` / `clova-to-openai`).
+ *
+ * v3 moves the model into the URL path (`/v3/chat-completions/{modelName}`), uses
+ * camelCase sampling params, and returns a `{status, result}` envelope instead of
+ * an OpenAI `choices[]` body — see the translators for the exact mapping.
+ *
+ * All three v3 models are live-verified against the real API (2026-09-01):
+ *
+ * | Model | Surface | Notes |
+ * | ------------- | -------- | -------------------------------------------------------- |
+ * | HCX-007 | thinking | rejects `maxTokens` (use `maxCompletionTokens`); no vision |
+ * | HCX-005 | text+img | vision via public URL **or** inline base64 data URI |
+ * | HCX-DASH-002 | text | lightweight, text only |
+ *
+ * Docs: https://api.ncloud-docs.com/docs/clovastudio-chatcompletionsv3
+ * https://api.ncloud-docs.com/docs/clovastudio-chatcompletionsv3-thinking
+ */
export const clova_studioProvider: RegistryEntry = {
id: "clova-studio",
alias: "clova",
- format: "openai",
- executor: "default",
- baseUrl: "https://clovastudio.stream.ntruss.com/v1/openai/chat/completions",
+ format: "clova",
+ executor: "clova-studio",
+ baseUrl: "https://clovastudio.stream.ntruss.com/v3/chat-completions",
authType: "apikey",
authHeader: "bearer",
+ /**
+ * The v3 API does answer non-streaming requests (`Accept: application/json`),
+ * but only the streaming surface is expressed in the translator: CLOVA's SSE
+ * frames carry incremental `token` events plus a terminal `result` event that
+ * repeats the full text. Forcing the upstream stream lets OmniRoute consume
+ * that single, well-tested path and accumulate it into a JSON body for
+ * non-streaming clients, instead of maintaining a second parser for the
+ * `{status, result}` envelope.
+ */
+ forceStream: true,
models: [
- // HCX-007 stays first so it remains the provider default (deep-reasoning
- // flagship); HCX-005 is the multimodal option.
- { id: "HCX-007", name: "HCX-007" },
- { id: "HCX-005", name: "HCX-005" },
+ {
+ // Reasoning flagship. Input+output ≤ 128000 tokens; the output cap counts
+ // thinking tokens too, so `maxCompletionTokens` may be up to 32768.
+ id: "HCX-007",
+ name: "HCX-007",
+ contextLength: 128000,
+ maxOutputTokens: 32768,
+ supportsReasoning: true,
+ },
+ {
+ // HyperCLOVA X vision model. Input+output ≤ 128000 tokens, output ≤ 4096,
+ // up to 5 images per request (1 per turn). Accepts a public URL or an
+ // inline base64 data URI — the data URI must keep its
+ // `data:;base64,` prefix inside `dataUri.data` or the request is
+ // rejected with `40001 Invalid parameter`.
+ id: "HCX-005",
+ name: "HCX-005",
+ contextLength: 128000,
+ maxOutputTokens: 4096,
+ supportsVision: true,
+ },
+ {
+ // Lightweight model. Input+output ≤ 32000 tokens, output ≤ 4096, text only.
+ id: "HCX-DASH-002",
+ name: "HCX-DASH-002",
+ contextLength: 32000,
+ maxOutputTokens: 4096,
+ },
],
};
diff --git a/open-sse/config/providers/registry/cursor/index.ts b/open-sse/config/providers/registry/cursor/index.ts
index cc5a78f8ce..4b5b3a86de 100644
--- a/open-sse/config/providers/registry/cursor/index.ts
+++ b/open-sse/config/providers/registry/cursor/index.ts
@@ -1,6 +1,37 @@
-import type { RegistryEntry } from "../../shared.ts";
+import type { RegistryEntry, RegistryModel } from "../../shared.ts";
import { CURSOR_REGISTRY_VERSION, getCursorRegistryHeaders } from "../../shared.ts";
+const CLAUDE_FABLE_5_1_CAPABILITIES = {
+ maxOutputTokens: 128_000,
+} as const;
+
+const ONE_MILLION_CONTEXT = 1_000_000;
+
+function withOneMillionContext(
+ models: RegistryModel[],
+ familyName: string,
+ defaultContextLength: number,
+ liveCatalogId: string,
+ supportsOneMillion: (model: RegistryModel) => boolean = () => true
+): RegistryModel[] {
+ return models.flatMap((model) => {
+ const defaultContextModel = {
+ ...model,
+ contextLength: defaultContextLength,
+ liveCatalogIds: model.liveCatalogIds ?? [liveCatalogId],
+ ...(familyName.startsWith("GPT-") ? {} : { scoresAs: model.scoresAs ?? liveCatalogId }),
+ };
+ if (!supportsOneMillion(model)) return [defaultContextModel];
+ const oneMillionModel = {
+ ...defaultContextModel,
+ id: `${model.id}-1m`,
+ name: model.name.replace(familyName, `${familyName} 1M`),
+ contextLength: ONE_MILLION_CONTEXT,
+ };
+ return [oneMillionModel, defaultContextModel];
+ });
+}
+
export const cursorProvider: RegistryEntry = {
id: "cursor",
alias: "cu",
@@ -18,259 +49,214 @@ export const cursorProvider: RegistryEntry = {
{ id: "auto-cost", name: "Auto (cost)" },
{ id: "auto-balance", name: "Auto (balance)" },
{ id: "auto-intelligence", name: "Auto (intelligence)" },
- // Legacy combo ids kept so existing cu/ targets are not orphaned.
- { id: "composer-2", name: "Composer 2" },
- { id: "composer-2-fast", name: "Composer 2 Fast" },
- { id: "gpt-5.4-low-fast", name: "GPT 5.4 Low Fast" },
- { id: "gpt-5.3-codex-spark-preview-low", name: "GPT 5.3 Codex Spark Preview Low" },
- { id: "gpt-5.3-codex-spark-preview", name: "GPT 5.3 Codex Spark Preview" },
- { id: "gpt-5.3-codex-spark-preview-high", name: "GPT 5.3 Codex Spark Preview High" },
- { id: "gpt-5.3-codex-spark-preview-xhigh", name: "GPT 5.3 Codex Spark Preview XHigh" },
- // #11489: cursor/agy spell Claude ids - ("claude-4.6-opus-high");
- // the effort splitter strips those to "claude-4.6-opus", which is not a catalog id.
- // `scoresAs` points each at the canonical - spelling so quality
- // scores are inherited. Operational metadata stays on these entries.
- {
- id: "claude-4.6-opus-high-thinking-fast",
- name: "Claude 4.6 Opus High Thinking Fast",
- scoresAs: "claude-opus-4-6",
- },
- {
- id: "claude-4.6-opus-max-thinking-fast",
- name: "Claude 4.6 Opus Max Thinking Fast",
- scoresAs: "claude-opus-4-6",
- },
- {
- id: "claude-4.6-sonnet-medium",
- name: "Claude 4.6 Sonnet Medium",
- scoresAs: "claude-sonnet-4-6",
- },
- {
- id: "claude-4.6-sonnet-medium-thinking",
- name: "Claude 4.6 Sonnet Medium Thinking",
- scoresAs: "claude-sonnet-4-6",
- },
- { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
- { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" },
- { id: "gemini-3-flash", name: "Gemini 3 Flash" },
- { id: "grok-4.6-medium", name: "Grok 4.6 Medium" },
- { id: "grok-4.6-fast-medium", name: "Grok 4.6 Fast Medium" },
- { id: "grok-4.6-high", name: "Grok 4.6 High" },
- { id: "grok-4.6-fast-high", name: "Grok 4.6 Fast High" },
- { id: "grok-4.6-xhigh", name: "Grok 4.6 XHigh" },
- { id: "grok-4.6-fast-xhigh", name: "Grok 4.6 Fast XHigh" },
- { id: "kimi-k3", name: "Kimi K3" },
- { id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
- { id: "grok-4.3", name: "Grok 4.3" },
- { id: "grok-4.5-medium", name: "Grok 4.5 Medium" },
- { id: "grok-4.5-fast-medium", name: "Grok 4.5 Fast Medium" },
- { id: "grok-4.5-high", name: "Grok 4.5 High" },
- { id: "grok-4.5-fast-high", name: "Grok 4.5 Fast High" },
- { id: "grok-4.5-xhigh", name: "Grok 4.5 XHigh" },
- { id: "grok-4.5-fast-xhigh", name: "Grok 4.5 Fast XHigh" },
- { id: "kimi-k2.5", name: "Kimi K2.5" },
- { id: "gpt-5.3-codex-low", name: "Codex 5.3 Low" },
- { id: "gpt-5.3-codex-low-fast", name: "Codex 5.3 Low Fast" },
- { id: "gpt-5.3-codex", name: "Codex 5.3" },
- { id: "gpt-5.3-codex-fast", name: "Codex 5.3 Fast" },
- { id: "gpt-5.3-codex-high", name: "Codex 5.3 High" },
- { id: "gpt-5.3-codex-high-fast", name: "Codex 5.3 High Fast" },
- { id: "gpt-5.3-codex-xhigh", name: "Codex 5.3 Extra High" },
- { id: "gpt-5.3-codex-xhigh-fast", name: "Codex 5.3 Extra High Fast" },
- { id: "gpt-5.2", name: "GPT-5.2" },
- { id: "cursor-grok-4.5-high", name: "Cursor Grok 4.5" },
- { id: "cursor-grok-4.5-high-fast", name: "Cursor Grok 4.5 Fast" },
- { id: "composer-2.5", name: "Composer 2.5" },
- { id: "claude-opus-5-thinking-high", name: "Opus 5 1M Thinking" },
- { id: "claude-opus-5-thinking-high-fast", name: "Opus 5 1M Thinking Fast" },
- { id: "claude-opus-5-thinking-xhigh", name: "Opus 5 1M Extra High Thinking" },
- { id: "claude-opus-5-thinking-xhigh-fast", name: "Opus 5 1M Extra High Thinking Fast" },
- { id: "claude-opus-4-8-thinking-high", name: "Opus 4.8 1M Thinking" },
- { id: "claude-opus-4-8-thinking-high-fast", name: "Opus 4.8 1M Thinking Fast" },
- { id: "gpt-5.6-sol-high", name: "GPT-5.6 Sol 1M High" },
- { id: "gpt-5.6-sol-high-fast", name: "GPT-5.6 Sol High Fast" },
- { id: "gpt-5.6-sol-xhigh", name: "GPT-5.6 Sol 1M Extra High" },
- { id: "gpt-5.6-sol-xhigh-fast", name: "GPT-5.6 Sol Extra High Fast" },
- { id: "gpt-5.5-high", name: "GPT-5.5 1M High" },
- { id: "gpt-5.5-high-fast", name: "GPT-5.5 High Fast" },
- { id: "claude-fable-5-thinking-high", name: "Fable 5 1M Thinking (NO ZDR)" },
- { id: "claude-fable-5-thinking-xhigh", name: "Fable 5 1M Extra High Thinking (NO ZDR)" },
- { id: "claude-sonnet-5-thinking-high", name: "Sonnet 5 1M Thinking" },
- { id: "claude-sonnet-5-thinking-xhigh", name: "Sonnet 5 1M Extra High Thinking" },
- { id: "kimi-k3-high", name: "Kimi K3 High" },
- { id: "cursor-grok-4.5-low", name: "Cursor Grok 4.5 Low" },
- { id: "cursor-grok-4.5-low-fast", name: "Cursor Grok 4.5 Low Fast" },
- { id: "cursor-grok-4.5-medium", name: "Cursor Grok 4.5 Medium" },
- { id: "cursor-grok-4.5-medium-fast", name: "Cursor Grok 4.5 Medium Fast" },
+ { id: "cursor-grok-4.6-xhigh-fast", name: "Cursor Grok 4.6 Xhigh Fast" },
+ { id: "cursor-grok-4.6-xhigh", name: "Cursor Grok 4.6 Xhigh" },
+ { id: "cursor-grok-4.6-high-fast", name: "Cursor Grok 4.6 High Fast" },
+ { id: "cursor-grok-4.6-high", name: "Cursor Grok 4.6 High" },
+ { id: "cursor-grok-4.6-medium-fast", name: "Cursor Grok 4.6 Medium Fast" },
+ { id: "cursor-grok-4.6-medium", name: "Cursor Grok 4.6 Medium" },
+ { id: "cursor-grok-4.6-low-fast", name: "Cursor Grok 4.6 Low Fast" },
+ { id: "cursor-grok-4.6-low", name: "Cursor Grok 4.6 Low" },
{ id: "composer-2.5-fast", name: "Composer 2.5 Fast" },
- { id: "claude-opus-5-low", name: "Opus 5 1M Low" },
- { id: "claude-opus-5-low-fast", name: "Opus 5 1M Low Fast" },
- { id: "claude-opus-5-medium", name: "Opus 5 1M Medium" },
- { id: "claude-opus-5-medium-fast", name: "Opus 5 1M Medium Fast" },
- { id: "claude-opus-5-high", name: "Opus 5 1M" },
- { id: "claude-opus-5-high-fast", name: "Opus 5 1M Fast" },
- { id: "claude-opus-5-thinking-low", name: "Opus 5 1M Low Thinking" },
- { id: "claude-opus-5-thinking-low-fast", name: "Opus 5 1M Low Thinking Fast" },
- { id: "claude-opus-5-thinking-medium", name: "Opus 5 1M Medium Thinking" },
- { id: "claude-opus-5-thinking-medium-fast", name: "Opus 5 1M Medium Thinking Fast" },
- { id: "claude-opus-5-thinking-max", name: "Opus 5 1M Max Thinking" },
- { id: "claude-opus-5-thinking-max-fast", name: "Opus 5 1M Max Thinking Fast" },
- { id: "claude-opus-4-8-low", name: "Opus 4.8 1M Low" },
- { id: "claude-opus-4-8-low-fast", name: "Opus 4.8 1M Low Fast" },
- { id: "claude-opus-4-8-medium", name: "Opus 4.8 1M Medium" },
- { id: "claude-opus-4-8-medium-fast", name: "Opus 4.8 1M Medium Fast" },
- { id: "claude-opus-4-8-high", name: "Opus 4.8 1M" },
- { id: "claude-opus-4-8-high-fast", name: "Opus 4.8 1M Fast" },
- { id: "claude-opus-4-8-xhigh", name: "Opus 4.8 1M Extra High" },
- { id: "claude-opus-4-8-xhigh-fast", name: "Opus 4.8 1M Extra High Fast" },
- { id: "claude-opus-4-8-max", name: "Opus 4.8 1M Max" },
- { id: "claude-opus-4-8-max-fast", name: "Opus 4.8 1M Max Fast" },
- { id: "claude-opus-4-8-thinking-low", name: "Opus 4.8 1M Low Thinking" },
- { id: "claude-opus-4-8-thinking-low-fast", name: "Opus 4.8 1M Low Thinking Fast" },
- { id: "claude-opus-4-8-thinking-medium", name: "Opus 4.8 1M Medium Thinking" },
- { id: "claude-opus-4-8-thinking-medium-fast", name: "Opus 4.8 1M Medium Thinking Fast" },
- { id: "claude-opus-4-8-thinking-xhigh", name: "Opus 4.8 1M Extra High Thinking" },
- { id: "claude-opus-4-8-thinking-xhigh-fast", name: "Opus 4.8 1M Extra High Thinking Fast" },
- { id: "claude-opus-4-8-thinking-max", name: "Opus 4.8 1M Max Thinking" },
- { id: "claude-opus-4-8-thinking-max-fast", name: "Opus 4.8 1M Max Thinking Fast" },
- { id: "gpt-5.6-sol-none", name: "GPT-5.6 Sol 1M None" },
- { id: "gpt-5.6-sol-none-fast", name: "GPT-5.6 Sol None Fast" },
- { id: "gpt-5.6-sol-low", name: "GPT-5.6 Sol 1M Low" },
- { id: "gpt-5.6-sol-low-fast", name: "GPT-5.6 Sol Low Fast" },
- { id: "gpt-5.6-sol-medium", name: "GPT-5.6 Sol 1M" },
- { id: "gpt-5.6-sol-medium-fast", name: "GPT-5.6 Sol Fast" },
- { id: "gpt-5.6-sol-max", name: "GPT-5.6 Sol 1M Max" },
- { id: "gpt-5.6-sol-max-fast", name: "GPT-5.6 Sol Max Fast" },
- { id: "gpt-5.5-none", name: "GPT-5.5 1M None" },
- { id: "gpt-5.5-none-fast", name: "GPT-5.5 None Fast" },
- { id: "gpt-5.5-low", name: "GPT-5.5 1M Low" },
- { id: "gpt-5.5-low-fast", name: "GPT-5.5 Low Fast" },
- { id: "gpt-5.5-medium", name: "GPT-5.5 1M" },
- { id: "gpt-5.5-medium-fast", name: "GPT-5.5 Fast" },
- { id: "gpt-5.5-extra-high", name: "GPT-5.5 1M Extra High" },
- { id: "gpt-5.5-extra-high-fast", name: "GPT-5.5 Extra High Fast" },
- { id: "claude-fable-5-low", name: "Fable 5 1M Low (NO ZDR)" },
- { id: "claude-fable-5-medium", name: "Fable 5 1M Medium (NO ZDR)" },
- { id: "claude-fable-5-high", name: "Fable 5 1M (NO ZDR)" },
- { id: "claude-fable-5-xhigh", name: "Fable 5 1M Extra High (NO ZDR)" },
- { id: "claude-fable-5-max", name: "Fable 5 1M Max (NO ZDR)" },
- { id: "claude-fable-5-thinking-low", name: "Fable 5 1M Low Thinking (NO ZDR)" },
- { id: "claude-fable-5-thinking-medium", name: "Fable 5 1M Medium Thinking (NO ZDR)" },
- { id: "claude-fable-5-thinking-max", name: "Fable 5 1M Max Thinking (NO ZDR)" },
- { id: "claude-sonnet-5-low", name: "Sonnet 5 1M Low" },
- { id: "claude-sonnet-5-medium", name: "Sonnet 5 1M Medium" },
- { id: "claude-sonnet-5-high", name: "Sonnet 5 1M" },
- { id: "claude-sonnet-5-xhigh", name: "Sonnet 5 1M Extra High" },
- { id: "claude-sonnet-5-max", name: "Sonnet 5 1M Max" },
- { id: "claude-sonnet-5-thinking-low", name: "Sonnet 5 1M Low Thinking" },
- { id: "claude-sonnet-5-thinking-medium", name: "Sonnet 5 1M Medium Thinking" },
- { id: "claude-sonnet-5-thinking-max", name: "Sonnet 5 1M Max Thinking" },
- { id: "gpt-5.6-terra-none", name: "GPT-5.6 Terra 1M None" },
- { id: "gpt-5.6-terra-none-fast", name: "GPT-5.6 Terra None Fast" },
- { id: "gpt-5.6-terra-low", name: "GPT-5.6 Terra 1M Low" },
- { id: "gpt-5.6-terra-low-fast", name: "GPT-5.6 Terra Low Fast" },
- { id: "gpt-5.6-terra-medium", name: "GPT-5.6 Terra 1M" },
- { id: "gpt-5.6-terra-medium-fast", name: "GPT-5.6 Terra Fast" },
- { id: "gpt-5.6-terra-high", name: "GPT-5.6 Terra 1M High" },
- { id: "gpt-5.6-terra-high-fast", name: "GPT-5.6 Terra High Fast" },
- { id: "gpt-5.6-terra-xhigh", name: "GPT-5.6 Terra 1M Extra High" },
- { id: "gpt-5.6-terra-xhigh-fast", name: "GPT-5.6 Terra Extra High Fast" },
- { id: "gpt-5.6-terra-max", name: "GPT-5.6 Terra 1M Max" },
- { id: "gpt-5.6-terra-max-fast", name: "GPT-5.6 Terra Max Fast" },
- { id: "claude-opus-4-7-low", name: "Opus 4.7 1M Low" },
- { id: "claude-opus-4-7-low-fast", name: "Opus 4.7 1M Low Fast" },
- { id: "claude-opus-4-7-medium", name: "Opus 4.7 1M Medium" },
- { id: "claude-opus-4-7-medium-fast", name: "Opus 4.7 1M Medium Fast" },
- { id: "claude-opus-4-7-high", name: "Opus 4.7 1M High" },
- { id: "claude-opus-4-7-high-fast", name: "Opus 4.7 1M High Fast" },
- { id: "claude-opus-4-7-xhigh", name: "Opus 4.7 1M" },
- { id: "claude-opus-4-7-xhigh-fast", name: "Opus 4.7 1M Fast" },
- { id: "claude-opus-4-7-max", name: "Opus 4.7 1M Max" },
- { id: "claude-opus-4-7-max-fast", name: "Opus 4.7 1M Max Fast" },
- { id: "claude-opus-4-7-thinking-low", name: "Opus 4.7 1M Low Thinking" },
- { id: "claude-opus-4-7-thinking-low-fast", name: "Opus 4.7 1M Low Thinking Fast" },
- { id: "claude-opus-4-7-thinking-medium", name: "Opus 4.7 1M Medium Thinking" },
- { id: "claude-opus-4-7-thinking-medium-fast", name: "Opus 4.7 1M Medium Thinking Fast" },
- { id: "claude-opus-4-7-thinking-high", name: "Opus 4.7 1M High Thinking" },
- { id: "claude-opus-4-7-thinking-high-fast", name: "Opus 4.7 1M High Thinking Fast" },
- { id: "claude-opus-4-7-thinking-xhigh", name: "Opus 4.7 1M Thinking" },
- { id: "claude-opus-4-7-thinking-xhigh-fast", name: "Opus 4.7 1M Thinking Fast" },
- { id: "claude-opus-4-7-thinking-max", name: "Opus 4.7 1M Max Thinking" },
- { id: "claude-opus-4-7-thinking-max-fast", name: "Opus 4.7 1M Max Thinking Fast" },
- { id: "gpt-5.4-low", name: "GPT-5.4 1M Low" },
- { id: "gpt-5.4-medium", name: "GPT-5.4 1M" },
- { id: "gpt-5.4-medium-fast", name: "GPT-5.4 Fast" },
- { id: "gpt-5.4-high", name: "GPT-5.4 1M High" },
- { id: "gpt-5.4-high-fast", name: "GPT-5.4 High Fast" },
- { id: "gpt-5.4-xhigh", name: "GPT-5.4 1M Extra High" },
- { id: "gpt-5.4-xhigh-fast", name: "GPT-5.4 Extra High Fast" },
- // #11489: cursor/agy spell Claude ids - ("claude-4.6-opus-high");
- // the effort splitter strips those to "claude-4.6-opus", which is not a catalog id.
- // `scoresAs` points each at the canonical - spelling so quality
- // scores are inherited. Operational metadata stays on these entries.
- { id: "claude-4.6-opus-high", name: "Opus 4.6 1M", scoresAs: "claude-opus-4-6" },
- { id: "claude-4.6-opus-max", name: "Opus 4.6 1M Max", scoresAs: "claude-opus-4-6" },
- {
- id: "claude-4.6-opus-high-thinking",
- name: "Opus 4.6 1M Thinking",
- scoresAs: "claude-opus-4-6",
- },
- {
- id: "claude-4.6-opus-max-thinking",
- name: "Opus 4.6 1M Max Thinking",
- scoresAs: "claude-opus-4-6",
- },
- { id: "claude-4.5-opus-high", name: "Opus 4.5", scoresAs: "claude-opus-4-5" },
- { id: "claude-4.5-opus-high-thinking", name: "Opus 4.5 Thinking", scoresAs: "claude-opus-4-5" },
- { id: "gpt-5.2-low", name: "GPT-5.2 Low" },
- { id: "gpt-5.2-low-fast", name: "GPT-5.2 Low Fast" },
- { id: "gpt-5.2-fast", name: "GPT-5.2 Fast" },
- { id: "gpt-5.2-high", name: "GPT-5.2 High" },
- { id: "gpt-5.2-high-fast", name: "GPT-5.2 High Fast" },
- { id: "gpt-5.2-xhigh", name: "GPT-5.2 Extra High" },
- { id: "gpt-5.2-xhigh-fast", name: "GPT-5.2 Extra High Fast" },
- { id: "gpt-5.6-luna-none", name: "GPT-5.6 Luna 1M None" },
- { id: "gpt-5.6-luna-none-fast", name: "GPT-5.6 Luna None Fast" },
- { id: "gpt-5.6-luna-low", name: "GPT-5.6 Luna 1M Low" },
- { id: "gpt-5.6-luna-low-fast", name: "GPT-5.6 Luna Low Fast" },
- { id: "gpt-5.6-luna-medium", name: "GPT-5.6 Luna 1M" },
- { id: "gpt-5.6-luna-medium-fast", name: "GPT-5.6 Luna Fast" },
- { id: "gpt-5.6-luna-high", name: "GPT-5.6 Luna 1M High" },
- { id: "gpt-5.6-luna-high-fast", name: "GPT-5.6 Luna High Fast" },
- { id: "gpt-5.6-luna-xhigh", name: "GPT-5.6 Luna 1M Extra High" },
- { id: "gpt-5.6-luna-xhigh-fast", name: "GPT-5.6 Luna Extra High Fast" },
- { id: "gpt-5.6-luna-max", name: "GPT-5.6 Luna 1M Max" },
- { id: "gpt-5.6-luna-max-fast", name: "GPT-5.6 Luna Max Fast" },
- { id: "gemini-3.6-flash-minimal", name: "Gemini 3.6 Flash Minimal" },
- { id: "gemini-3.6-flash-low", name: "Gemini 3.6 Flash Low" },
- { id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash Medium" },
- { id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash" },
- { id: "gpt-5.4-mini-none", name: "GPT-5.4 Mini None" },
- { id: "gpt-5.4-mini-low", name: "GPT-5.4 Mini Low" },
- { id: "gpt-5.4-mini-medium", name: "GPT-5.4 Mini" },
- { id: "gpt-5.4-mini-high", name: "GPT-5.4 Mini High" },
- { id: "gpt-5.4-mini-xhigh", name: "GPT-5.4 Mini Extra High" },
- { id: "gpt-5.4-nano-none", name: "GPT-5.4 Nano None" },
- { id: "gpt-5.4-nano-low", name: "GPT-5.4 Nano Low" },
- { id: "gpt-5.4-nano-medium", name: "GPT-5.4 Nano" },
- { id: "gpt-5.4-nano-high", name: "GPT-5.4 Nano High" },
- { id: "gpt-5.4-nano-xhigh", name: "GPT-5.4 Nano Extra High" },
- { id: "claude-4.5-sonnet", name: "Sonnet 4.5", scoresAs: "claude-sonnet-4-5" },
- {
- id: "claude-4.5-sonnet-thinking",
- name: "Sonnet 4.5 Thinking",
- scoresAs: "claude-sonnet-4-5",
- },
- { id: "gpt-5.1-low", name: "GPT-5.1 Low" },
- { id: "gpt-5.1", name: "GPT-5.1" },
- { id: "gpt-5.1-high", name: "GPT-5.1 High" },
- { id: "claude-4-sonnet", name: "Sonnet 4", scoresAs: "claude-sonnet-4" },
- { id: "claude-4-sonnet-thinking", name: "Sonnet 4 Thinking", scoresAs: "claude-sonnet-4" },
- { id: "gpt-5-mini", name: "GPT-5 Mini" },
+ { id: "composer-2.5", name: "Composer 2.5" },
+ ...withOneMillionContext(
+ [
+ {
+ id: "claude-fable-5-1-thinking-max",
+ name: "Claude Fable 5.1 Max Thinking",
+ ...CLAUDE_FABLE_5_1_CAPABILITIES,
+ },
+ {
+ id: "claude-fable-5-1-thinking-xhigh",
+ name: "Claude Fable 5.1 Xhigh Thinking",
+ ...CLAUDE_FABLE_5_1_CAPABILITIES,
+ },
+ {
+ id: "claude-fable-5-1-thinking-high",
+ name: "Claude Fable 5.1 High Thinking",
+ ...CLAUDE_FABLE_5_1_CAPABILITIES,
+ },
+ {
+ id: "claude-fable-5-1-thinking-medium",
+ name: "Claude Fable 5.1 Medium Thinking",
+ ...CLAUDE_FABLE_5_1_CAPABILITIES,
+ },
+ {
+ id: "claude-fable-5-1-thinking-low",
+ name: "Claude Fable 5.1 Low Thinking",
+ ...CLAUDE_FABLE_5_1_CAPABILITIES,
+ },
+ ],
+ "Claude Fable 5.1",
+ 300_000,
+ "claude-fable-5-1"
+ ),
+ ...withOneMillionContext(
+ [
+ { id: "claude-opus-5-thinking-max-fast", name: "Claude Opus 5 Max Thinking Fast" },
+ { id: "claude-opus-5-thinking-max", name: "Claude Opus 5 Max Thinking" },
+ {
+ id: "claude-opus-5-thinking-xhigh-fast",
+ name: "Claude Opus 5 Xhigh Thinking Fast",
+ },
+ { id: "claude-opus-5-thinking-xhigh", name: "Claude Opus 5 Xhigh Thinking" },
+ { id: "claude-opus-5-thinking-high-fast", name: "Claude Opus 5 High Thinking Fast" },
+ { id: "claude-opus-5-thinking-high", name: "Claude Opus 5 High Thinking" },
+ { id: "claude-opus-5-high-fast", name: "Claude Opus 5 High Fast" },
+ { id: "claude-opus-5-high", name: "Claude Opus 5 High" },
+ {
+ id: "claude-opus-5-thinking-medium-fast",
+ name: "Claude Opus 5 Medium Thinking Fast",
+ },
+ { id: "claude-opus-5-thinking-medium", name: "Claude Opus 5 Medium Thinking" },
+ { id: "claude-opus-5-medium-fast", name: "Claude Opus 5 Medium Fast" },
+ { id: "claude-opus-5-medium", name: "Claude Opus 5 Medium" },
+ { id: "claude-opus-5-thinking-low-fast", name: "Claude Opus 5 Low Thinking Fast" },
+ { id: "claude-opus-5-thinking-low", name: "Claude Opus 5 Low Thinking" },
+ { id: "claude-opus-5-low-fast", name: "Claude Opus 5 Low Fast" },
+ { id: "claude-opus-5-low", name: "Claude Opus 5 Low" },
+ ],
+ "Claude Opus 5",
+ 300_000,
+ "claude-opus-5"
+ ),
+ ...withOneMillionContext(
+ [
+ { id: "claude-opus-4-8-thinking-max-fast", name: "Claude Opus 4.8 Max Thinking Fast" },
+ { id: "claude-opus-4-8-thinking-max", name: "Claude Opus 4.8 Max Thinking" },
+ { id: "claude-opus-4-8-max-fast", name: "Claude Opus 4.8 Max Fast" },
+ { id: "claude-opus-4-8-max", name: "Claude Opus 4.8 Max" },
+ {
+ id: "claude-opus-4-8-thinking-xhigh-fast",
+ name: "Claude Opus 4.8 Xhigh Thinking Fast",
+ },
+ { id: "claude-opus-4-8-thinking-xhigh", name: "Claude Opus 4.8 Xhigh Thinking" },
+ { id: "claude-opus-4-8-xhigh-fast", name: "Claude Opus 4.8 Xhigh Fast" },
+ { id: "claude-opus-4-8-xhigh", name: "Claude Opus 4.8 Xhigh" },
+ { id: "claude-opus-4-8-thinking-high-fast", name: "Claude Opus 4.8 High Thinking Fast" },
+ { id: "claude-opus-4-8-thinking-high", name: "Claude Opus 4.8 High Thinking" },
+ { id: "claude-opus-4-8-high-fast", name: "Claude Opus 4.8 High Fast" },
+ { id: "claude-opus-4-8-high", name: "Claude Opus 4.8 High" },
+ {
+ id: "claude-opus-4-8-thinking-medium-fast",
+ name: "Claude Opus 4.8 Medium Thinking Fast",
+ },
+ { id: "claude-opus-4-8-thinking-medium", name: "Claude Opus 4.8 Medium Thinking" },
+ { id: "claude-opus-4-8-medium-fast", name: "Claude Opus 4.8 Medium Fast" },
+ { id: "claude-opus-4-8-medium", name: "Claude Opus 4.8 Medium" },
+ { id: "claude-opus-4-8-thinking-low-fast", name: "Claude Opus 4.8 Low Thinking Fast" },
+ { id: "claude-opus-4-8-thinking-low", name: "Claude Opus 4.8 Low Thinking" },
+ { id: "claude-opus-4-8-low-fast", name: "Claude Opus 4.8 Low Fast" },
+ { id: "claude-opus-4-8-low", name: "Claude Opus 4.8 Low" },
+ ],
+ "Claude Opus 4.8",
+ 300_000,
+ "claude-opus-4-8"
+ ),
+ ...withOneMillionContext(
+ [
+ { id: "claude-sonnet-5-thinking-max", name: "Claude Sonnet 5 Max Thinking" },
+ { id: "claude-sonnet-5-max", name: "Claude Sonnet 5 Max" },
+ { id: "claude-sonnet-5-thinking-xhigh", name: "Claude Sonnet 5 Xhigh Thinking" },
+ { id: "claude-sonnet-5-xhigh", name: "Claude Sonnet 5 Xhigh" },
+ { id: "claude-sonnet-5-thinking-high", name: "Claude Sonnet 5 High Thinking" },
+ { id: "claude-sonnet-5-high", name: "Claude Sonnet 5 High" },
+ { id: "claude-sonnet-5-thinking-medium", name: "Claude Sonnet 5 Medium Thinking" },
+ { id: "claude-sonnet-5-medium", name: "Claude Sonnet 5 Medium" },
+ { id: "claude-sonnet-5-thinking-low", name: "Claude Sonnet 5 Low Thinking" },
+ { id: "claude-sonnet-5-low", name: "Claude Sonnet 5 Low" },
+ ],
+ "Claude Sonnet 5",
+ 300_000,
+ "claude-sonnet-5"
+ ),
+ ...withOneMillionContext(
+ [
+ { id: "claude-4.6-sonnet-max-thinking", name: "Claude Sonnet 4.6 Max Thinking" },
+ { id: "claude-4.6-sonnet-max", name: "Claude Sonnet 4.6 Max" },
+ { id: "claude-4.6-sonnet-high-thinking", name: "Claude Sonnet 4.6 High Thinking" },
+ { id: "claude-4.6-sonnet-high", name: "Claude Sonnet 4.6 High" },
+ { id: "claude-4.6-sonnet-medium-thinking", name: "Claude Sonnet 4.6 Medium Thinking" },
+ { id: "claude-4.6-sonnet-medium", name: "Claude Sonnet 4.6 Medium" },
+ { id: "claude-4.6-sonnet-low-thinking", name: "Claude Sonnet 4.6 Low Thinking" },
+ { id: "claude-4.6-sonnet-low", name: "Claude Sonnet 4.6 Low" },
+ ],
+ "Claude Sonnet 4.6",
+ 200_000,
+ "claude-sonnet-4-6"
+ ),
+ { id: "claude-4.5-haiku-thinking", name: "Claude Haiku 4.5 Thinking" },
+ { id: "claude-4.5-haiku", name: "Claude Haiku 4.5" },
+ ...withOneMillionContext(
+ [
+ { id: "gpt-5.6-sol-max-fast", name: "GPT-5.6 Sol Max Fast" },
+ { id: "gpt-5.6-sol-max", name: "GPT-5.6 Sol Max" },
+ { id: "gpt-5.6-sol-xhigh-fast", name: "GPT-5.6 Sol Xhigh Fast" },
+ { id: "gpt-5.6-sol-xhigh", name: "GPT-5.6 Sol Xhigh" },
+ { id: "gpt-5.6-sol-high-fast", name: "GPT-5.6 Sol High Fast" },
+ { id: "gpt-5.6-sol-high", name: "GPT-5.6 Sol High" },
+ { id: "gpt-5.6-sol-medium-fast", name: "GPT-5.6 Sol Medium Fast" },
+ { id: "gpt-5.6-sol-medium", name: "GPT-5.6 Sol Medium" },
+ { id: "gpt-5.6-sol-low-fast", name: "GPT-5.6 Sol Low Fast" },
+ { id: "gpt-5.6-sol-low", name: "GPT-5.6 Sol Low" },
+ { id: "gpt-5.6-sol-none-fast", name: "GPT-5.6 Sol None Fast" },
+ { id: "gpt-5.6-sol-none", name: "GPT-5.6 Sol None" },
+ ],
+ "GPT-5.6 Sol",
+ 272_000,
+ "gpt-5.6-sol",
+ (model) => !model.id.endsWith("-fast")
+ ),
+ ...withOneMillionContext(
+ [
+ { id: "gpt-5.6-terra-max-fast", name: "GPT-5.6 Terra Max Fast" },
+ { id: "gpt-5.6-terra-max", name: "GPT-5.6 Terra Max" },
+ { id: "gpt-5.6-terra-xhigh-fast", name: "GPT-5.6 Terra Xhigh Fast" },
+ { id: "gpt-5.6-terra-xhigh", name: "GPT-5.6 Terra Xhigh" },
+ { id: "gpt-5.6-terra-high-fast", name: "GPT-5.6 Terra High Fast" },
+ { id: "gpt-5.6-terra-high", name: "GPT-5.6 Terra High" },
+ { id: "gpt-5.6-terra-medium-fast", name: "GPT-5.6 Terra Medium Fast" },
+ { id: "gpt-5.6-terra-medium", name: "GPT-5.6 Terra Medium" },
+ { id: "gpt-5.6-terra-low-fast", name: "GPT-5.6 Terra Low Fast" },
+ { id: "gpt-5.6-terra-low", name: "GPT-5.6 Terra Low" },
+ { id: "gpt-5.6-terra-none-fast", name: "GPT-5.6 Terra None Fast" },
+ { id: "gpt-5.6-terra-none", name: "GPT-5.6 Terra None" },
+ ],
+ "GPT-5.6 Terra",
+ 272_000,
+ "gpt-5.6-terra",
+ (model) => !model.id.endsWith("-fast")
+ ),
+ ...withOneMillionContext(
+ [
+ { id: "gpt-5.6-luna-max-fast", name: "GPT-5.6 Luna Max Fast" },
+ { id: "gpt-5.6-luna-max", name: "GPT-5.6 Luna Max" },
+ { id: "gpt-5.6-luna-xhigh-fast", name: "GPT-5.6 Luna Xhigh Fast" },
+ { id: "gpt-5.6-luna-xhigh", name: "GPT-5.6 Luna Xhigh" },
+ { id: "gpt-5.6-luna-high-fast", name: "GPT-5.6 Luna High Fast" },
+ { id: "gpt-5.6-luna-high", name: "GPT-5.6 Luna High" },
+ { id: "gpt-5.6-luna-medium-fast", name: "GPT-5.6 Luna Medium Fast" },
+ { id: "gpt-5.6-luna-medium", name: "GPT-5.6 Luna Medium" },
+ { id: "gpt-5.6-luna-low-fast", name: "GPT-5.6 Luna Low Fast" },
+ { id: "gpt-5.6-luna-low", name: "GPT-5.6 Luna Low" },
+ { id: "gpt-5.6-luna-none-fast", name: "GPT-5.6 Luna None Fast" },
+ { id: "gpt-5.6-luna-none", name: "GPT-5.6 Luna None" },
+ ],
+ "GPT-5.6 Luna",
+ 272_000,
+ "gpt-5.6-luna",
+ (model) => !model.id.endsWith("-fast")
+ ),
+ { id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash High" },
+ { id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash Medium" },
+ { id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash Low" },
+ { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
+ { id: "kimi-k3-max", name: "Kimi K3 Max" },
+ { id: "kimi-k3-high", name: "Kimi K3 High" },
{ id: "kimi-k3-low", name: "Kimi K3 Low" },
- { id: "kimi-k3-max", name: "Kimi K3" },
- { id: "glm-5.2-high", name: "GLM 5.2" },
+ { id: "kimi-k2.7-code", name: "Kimi K2.7 Code" },
{ id: "glm-5.2-max", name: "GLM 5.2 Max" },
+ { id: "glm-5.2-high", name: "GLM 5.2 High" },
],
};
diff --git a/open-sse/config/providers/registry/devin/catalog.ts b/open-sse/config/providers/registry/devin/catalog.ts
index d1e5c75894..6ab53fda12 100644
--- a/open-sse/config/providers/registry/devin/catalog.ts
+++ b/open-sse/config/providers/registry/devin/catalog.ts
@@ -1,115 +1,150 @@
import type { RegistryModel } from "../../shared.ts";
+type EffortVariant = readonly [suffix: string, label: string];
+
+const QUALITY_EFFORTS: readonly EffortVariant[] = [
+ ["max", "Max"],
+ ["xhigh", "XHigh"],
+ ["high", "High"],
+ ["medium", "Medium"],
+ ["low", "Low"],
+];
+
+const GPT_EFFORTS: readonly EffortVariant[] = [
+ ["max", "Max Thinking"],
+ ["xhigh", "XHigh Thinking"],
+ ["high", "High Thinking"],
+ ["medium", "Medium Thinking"],
+ ["low", "Low Thinking"],
+ ["none", "No Thinking"],
+];
+
+function model(
+ id: string,
+ name: string,
+ maxOutputTokens?: number,
+ contextLength?: number
+): RegistryModel {
+ return {
+ id,
+ name,
+ ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
+ ...(contextLength === undefined ? {} : { contextLength }),
+ };
+}
+
+function effortModels(
+ id: string,
+ name: string,
+ maxOutputTokens: number,
+ contextLength: number | undefined,
+ efforts: readonly EffortVariant[] = QUALITY_EFFORTS
+): RegistryModel[] {
+ return efforts.map(([suffix, label]) =>
+ model(`${id}-${suffix}`, `${name} ${label}`, maxOutputTokens, contextLength)
+ );
+}
+
+function fastEffortModels(
+ id: string,
+ name: string,
+ maxOutputTokens: number,
+ contextLength: number
+): RegistryModel[] {
+ return QUALITY_EFFORTS.flatMap(([suffix, label]) => [
+ model(`${id}-${suffix}-fast`, `${name} ${label} Fast`, maxOutputTokens, contextLength),
+ model(`${id}-${suffix}`, `${name} ${label}`, maxOutputTokens, contextLength),
+ ]);
+}
+
+function gptModels(id: string, name: string): RegistryModel[] {
+ return GPT_EFFORTS.flatMap(([suffix, label]) => [
+ model(`${id}-${suffix}-priority`, `${name} ${label} Fast`, 128_000, 1_000_000),
+ model(`${id}-${suffix}`, `${name} ${label}`, 128_000, 1_000_000),
+ ]);
+}
+
+/**
+ * Curated from the authenticated `devin models list --format json` response on
+ * 2026-09-02. Keep this deliberately smaller than Devin's full live catalog:
+ * these are the operator-selected models OmniRoute intends to expose.
+ */
export const DEVIN_MODEL_CATALOG: RegistryModel[] = [
- // Cognition / SWE — default model family recommended for coding tasks
- { id: "swe-1-7-lightning", name: "SWE-1.7 Lightning", contextLength: 202752 },
- { id: "swe-1-7", name: "SWE-1.7", contextLength: 262000 },
- { id: "swe-1-6-fast", name: "SWE-1.6 Fast" },
- { id: "swe-1-6", name: "SWE-1.6" },
- // Claude Fable 5
- { id: "claude-5-fable-max", name: "Claude Fable 5 Max", contextLength: 1000000 },
- { id: "claude-5-fable-xhigh", name: "Claude Fable 5 XHigh", contextLength: 1000000 },
- { id: "claude-5-fable-high", name: "Claude Fable 5 High", contextLength: 1000000 },
- { id: "claude-5-fable-medium", name: "Claude Fable 5 Medium", contextLength: 1000000 },
- { id: "claude-5-fable-low", name: "Claude Fable 5 Low", contextLength: 1000000 },
- // Claude Opus 5
- { id: "claude-opus-5-max", name: "Claude Opus 5 Max", contextLength: 1000000 },
- { id: "claude-opus-5-xhigh", name: "Claude Opus 5 XHigh", contextLength: 1000000 },
- { id: "claude-opus-5-high", name: "Claude Opus 5 High", contextLength: 1000000 },
- { id: "claude-opus-5-medium", name: "Claude Opus 5 Medium", contextLength: 1000000 },
- { id: "claude-opus-5-low", name: "Claude Opus 5 Low", contextLength: 1000000 },
- // Claude Opus 4.8
- { id: "claude-opus-4-8-max", name: "Claude Opus 4.8 Max", contextLength: 1000000 },
- { id: "claude-opus-4-8-xhigh", name: "Claude Opus 4.8 XHigh", contextLength: 1000000 },
- { id: "claude-opus-4-8-high", name: "Claude Opus 4.8 High", contextLength: 1000000 },
- { id: "claude-opus-4-8-medium", name: "Claude Opus 4.8 Medium", contextLength: 1000000 },
- { id: "claude-opus-4-8-low", name: "Claude Opus 4.8 Low", contextLength: 1000000 },
- // Claude Opus 4.7
- { id: "claude-opus-4-7-max", name: "Claude Opus 4.7 Max", contextLength: 1000000 },
- { id: "claude-opus-4-7-xhigh", name: "Claude Opus 4.7 XHigh", contextLength: 1000000 },
- { id: "claude-opus-4-7-high", name: "Claude Opus 4.7 High", contextLength: 1000000 },
- { id: "claude-opus-4-7-medium", name: "Claude Opus 4.7 Medium", contextLength: 1000000 },
- { id: "claude-opus-4-7-low", name: "Claude Opus 4.7 Low", contextLength: 1000000 },
- // Claude Opus 4.6
- {
- id: "claude-opus-4-6-thinking-1m",
- name: "Claude Opus 4.6 Thinking 1M",
- contextLength: 1000000,
- },
- { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking", contextLength: 200000 },
- { id: "claude-opus-4-6-1m", name: "Claude Opus 4.6 1M", contextLength: 1000000 },
- { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextLength: 200000 },
- // Claude Sonnet 5
- { id: "claude-sonnet-5-max", name: "Claude Sonnet 5 Max", contextLength: 1000000 },
- { id: "claude-sonnet-5-xhigh", name: "Claude Sonnet 5 XHigh", contextLength: 1000000 },
- { id: "claude-sonnet-5-high", name: "Claude Sonnet 5 High", contextLength: 1000000 },
- { id: "claude-sonnet-5-medium", name: "Claude Sonnet 5 Medium", contextLength: 1000000 },
- { id: "claude-sonnet-5-low", name: "Claude Sonnet 5 Low", contextLength: 1000000 },
- // Claude Sonnet 4.6
- {
- id: "claude-sonnet-4-6-thinking-1m",
- name: "Claude Sonnet 4.6 Thinking 1M",
- contextLength: 1000000,
- },
- {
- id: "claude-sonnet-4-6-thinking",
- name: "Claude Sonnet 4.6 Thinking",
- contextLength: 200000,
- },
- { id: "claude-sonnet-4-6-1m", name: "Claude Sonnet 4.6 1M", contextLength: 1000000 },
- { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 200000 },
- // GPT-5.6
- { id: "gpt-5-6-sol-max", name: "GPT-5.6 Sol Max", contextLength: 1000000 },
- { id: "gpt-5-6-sol-xhigh", name: "GPT-5.6 Sol XHigh", contextLength: 1000000 },
- { id: "gpt-5-6-sol-high", name: "GPT-5.6 Sol High", contextLength: 1000000 },
- { id: "gpt-5-6-sol-medium", name: "GPT-5.6 Sol Medium", contextLength: 1000000 },
- { id: "gpt-5-6-sol-low", name: "GPT-5.6 Sol Low", contextLength: 1000000 },
- /// Terra
- { id: "gpt-5-6-terra-max", name: "GPT-5.6 Terra Max", contextLength: 1000000 },
- { id: "gpt-5-6-terra-xhigh", name: "GPT-5.6 Terra XHigh", contextLength: 1000000 },
- { id: "gpt-5-6-terra-high", name: "GPT-5.6 Terra High", contextLength: 1000000 },
- { id: "gpt-5-6-terra-medium", name: "GPT-5.6 Terra Medium", contextLength: 1000000 },
- { id: "gpt-5-6-terra-low", name: "GPT-5.6 Terra Low", contextLength: 1000000 },
- /// Luna
- { id: "gpt-5-6-luna-max", name: "GPT-5.6 Luna Max", contextLength: 1000000 },
- { id: "gpt-5-6-luna-xhigh", name: "GPT-5.6 Luna XHigh", contextLength: 1000000 },
- { id: "gpt-5-6-luna-high", name: "GPT-5.6 Luna High", contextLength: 1000000 },
- { id: "gpt-5-6-luna-medium", name: "GPT-5.6 Luna Medium", contextLength: 1000000 },
- { id: "gpt-5-6-luna-low", name: "GPT-5.6 Luna Low", contextLength: 1000000 },
- // GPT-5.5
- { id: "gpt-5-5-xhigh", name: "GPT-5.5 XHigh", contextLength: 272000 },
- { id: "gpt-5-5-high", name: "GPT-5.5 High", contextLength: 272000 },
- { id: "gpt-5-5-medium", name: "GPT-5.5 Medium", contextLength: 272000 },
- { id: "gpt-5-5-low", name: "GPT-5.5 Low", contextLength: 272000 },
- // Gemini
- { id: "gemini-3-1-pro-high", name: "Gemini 3.1 Pro High", contextLength: 1048576 },
- { id: "gemini-3-1-pro-low", name: "Gemini 3.1 Pro Low", contextLength: 1048576 },
- { id: "gemini-3-7-flash-high", name: "Gemini 3.7 Flash High" },
- { id: "gemini-3-7-flash-medium", name: "Gemini 3.7 Flash Medium" },
- { id: "gemini-3-7-flash-low", name: "Gemini 3.7 Flash Low" },
- { id: "gemini-3-7-flash-minimal", name: "Gemini 3.7 Flash Minimal" },
- // Grok
- { id: "grok-4-5-high", name: "Grok 4.5 High", contextLength: 500000 },
- { id: "grok-4-5-medium", name: "Grok 4.5 Medium", contextLength: 500000 },
- { id: "grok-4-5-low", name: "Grok 4.5 Low", contextLength: 500000 },
- // GLM
- { id: "glm-5-2-max-1m", name: "GLM-5.2 Max 1M", contextLength: 1000000 },
- { id: "glm-5-2-max", name: "GLM-5.2 Max" },
- { id: "glm-5-2-1m", name: "GLM-5.2 High 1M", contextLength: 1000000 },
- { id: "glm-5-2", name: "GLM-5.2 High" },
- // Kimi
- { id: "kimi-k3-max", name: "Kimi K3 Max" },
- { id: "kimi-k3-high", name: "Kimi K3 High" },
- { id: "kimi-k3-low", name: "Kimi K3 Low" },
- { id: "kimi-k2-7", name: "Kimi K2.7", contextLength: 262144 },
- // Inkling
- { id: "inkling-max", name: "Inkling Max" },
- { id: "inkling-xhigh", name: "Inkling XHigh" },
- { id: "inkling-high", name: "Inkling High" },
- { id: "inkling-medium", name: "Inkling Medium" },
- { id: "inkling-low", name: "Inkling Low" },
- { id: "inkling-none", name: "Inkling None" },
- // Others
- { id: "deepseek-v4", name: "DeepSeek V4 Pro", contextLength: 1048576 },
- { id: "nemotron-3-ultra-nvfp4", name: "Nemotron 3 Ultra", contextLength: 262144 },
+ ...effortModels("claude-fable-5-1", "Claude Fable 5.1", 128_000, 1_000_000),
+ ...fastEffortModels("claude-opus-5", "Claude Opus 5", 128_000, 1_000_000),
+ ...fastEffortModels("claude-opus-4-8", "Claude Opus 4.8", 128_000, 1_000_000),
+ ...effortModels("claude-sonnet-5", "Claude Sonnet 5", 128_000, 1_000_000),
+
+ model("claude-sonnet-4-6-thinking-1m", "Claude Sonnet 4.6 Thinking 1M", 128_000, 1_000_000),
+ model("claude-sonnet-4-6-1m", "Claude Sonnet 4.6 1M", 128_000, 1_000_000),
+ model("claude-sonnet-4-6-thinking", "Claude Sonnet 4.6 Thinking", 128_000, 200_000),
+ model("claude-sonnet-4-6", "Claude Sonnet 4.6", 128_000, 200_000),
+ model("MODEL_PRIVATE_11", "Claude Haiku 4.5", 64_000, 200_000),
+
+ ...gptModels("gpt-5-6-sol", "GPT-5.6 Sol"),
+ ...gptModels("gpt-5-6-terra", "GPT-5.6 Terra"),
+ ...gptModels("gpt-5-6-luna", "GPT-5.6 Luna"),
+
+ ...effortModels("kimi-k3", "Kimi K3", 131_072, 1_048_576, [
+ ["max", "Max"],
+ ["high", "High"],
+ ["low", "Low"],
+ ]),
+ model("kimi-k2-7", "Kimi K2.7", 16_000, 262_144),
+
+ ...effortModels("glm-5-3", "GLM-5.3", 128_000, 1_000_000, [
+ ["max", "Max"],
+ ["high", "High"],
+ ["low", "Low"],
+ ]),
+ ...effortModels("glm-5-3-flash", "GLM-5.3 Flash", 128_000, 1_000_000, [
+ ["max", "Max"],
+ ["high", "High"],
+ ["low", "Low"],
+ ]),
+
+ model("swe-1-7", "SWE-1.7 Max", 128_000, 262_000),
+ model("swe-1-7-medium", "SWE-1.7 Medium", 128_000, 262_000),
+ model("swe-1-7-lightning", "SWE-1.7 Lightning Max", 96_000, 202_752),
+ model("swe-1-7-lightning-medium", "SWE-1.7 Lightning Medium", 96_000, 202_752),
+ model("adaptive", "Adaptive"),
+
+ ...effortModels("grok-4-6", "Grok 4.6", 100_000, 500_000, [
+ ["xhigh", "XHigh"],
+ ["high", "High"],
+ ["medium", "Medium"],
+ ["low", "Low"],
+ ]),
+ ...effortModels("inkling", "Inkling", 131_072, undefined, [
+ ["max", "Max"],
+ ["xhigh", "X-High"],
+ ["high", "High"],
+ ["medium", "Medium"],
+ ["low", "Low"],
+ ["none", "None"],
+ ]),
+ ...effortModels("deepseek-v4-flash", "DeepSeek V4 Flash", 384_000, 1_000_000, [
+ ["max", "Max"],
+ ["high", "High"],
+ ["low", "Low"],
+ ]),
+ ...effortModels("nemotron-3-ultra", "Nemotron 3 Ultra", 32_768, 262_144, [
+ ["high", "High"],
+ ["medium", "Medium"],
+ ["none", "None"],
+ ]),
+ ...effortModels("gemini-3-7-flash", "Gemini 3.7 Flash", 65_535, 1_048_576, [
+ ["high", "High"],
+ ["medium", "Medium"],
+ ["low", "Low"],
+ ]),
+ ...effortModels("gemini-3-1-pro", "Gemini 3.1 Pro", 65_535, 1_048_576, [
+ ["high", "High Thinking"],
+ ["low", "Low Thinking"],
+ ]),
+ ...effortModels("deepseek-v4-pro", "DeepSeek V4 Pro", 384_000, 1_000_000, [
+ ["max", "Max"],
+ ["high", "High"],
+ ["low", "Low"],
+ ]),
];
diff --git a/open-sse/config/providers/registry/nvidia/index.ts b/open-sse/config/providers/registry/nvidia/index.ts
index 0966fa5941..dab45294d3 100644
--- a/open-sse/config/providers/registry/nvidia/index.ts
+++ b/open-sse/config/providers/registry/nvidia/index.ts
@@ -9,104 +9,34 @@ export const nvidiaProvider: RegistryEntry = {
authType: "apikey",
authHeader: "bearer",
toolNameMaxLength: 64,
- // #6773: nvidia multiplexes 17 models from 9 different upstream vendors
- // (z-ai/, minimaxai/, deepseek-ai/, qwen/, mistralai/, stepfun-ai/,
- // moonshotai/, openai/, nvidia/) behind ONE connection — mark it passthrough
+ // #6773: NVIDIA multiplexes models from multiple upstream vendors
+ // (moonshotai/, deepseek-ai/, nvidia/, meta/, poolside/, google/, openai/)
+ // behind ONE connection — mark it passthrough
// so a single stale/renamed model's 404 locks out only that model instead
// of cooling down the whole connection (see accountFallback.ts
// hasPerModelQuota doc comment; matches modelscope/synthetic/kilo-gateway).
passthroughModels: true,
models: [
- // #6108: z-ai/glm-5.1 EOL'd 2026-07-02 (direct probe returns 410) — dropped.
- // #10788: NVIDIA's hosted GLM-5.2 exposes a BINARY thinking switch
- // (chat_template_kwargs.enable_thinking), not effort tiers — see
- // mapNvidiaGlm52ReasoningParams. Declaring an empty tier list keeps the
- // catalog from synthesizing unresolvable -low/-high/-max variant ids while
- // still marking the model reasoning-capable.
+ { id: "moonshotai/kimi-k3", name: "Kimi K3" },
{
- id: "z-ai/glm-5.2",
- name: "GLM 5.2",
+ id: "deepseek-ai/deepseek-v4-pro-0813",
+ name: "DeepSeek V4 Pro 0813",
supportsReasoning: true,
- supportedThinkingEfforts: [],
},
- // #3329/#6108: minimaxai/minimax-m3 stays excluded from the nvidia tier — it
- // still 404s here for most callers; the single 200 probe in #6108 was not
- // reproducible enough to override the #3329 guard. Re-add only once NVIDIA
- // reliably serves it (and flip nvidia-minimax-m3-removed-3329.test.ts then).
- { id: "minimaxai/minimax-m2.7", name: "MiniMax M2.7" },
+ {
+ id: "deepseek-ai/deepseek-v4-flash-0731",
+ name: "DeepSeek V4 Flash 0731",
+ supportsReasoning: true,
+ },
+ { id: "meta/muse-glimmer-30b", name: "Muse Glimmer 30B" },
+ { id: "poolside/laguna-xs-2.1", name: "Laguna XS 2.1" },
{ id: "google/gemma-4-31b-it", name: "Gemma 4 31B" },
- { id: "mistralai/mistral-small-4-119b-2603", name: "Mistral Small 4 2603" },
- { id: "mistralai/mistral-large-3-675b-instruct-2512", name: "Mistral Large 3 675B" },
- { id: "mistralai/devstral-2-123b-instruct-2512", name: "Devstral 2 123B" },
- { id: "qwen/qwen3.5-397b-a17b", name: "Qwen3.5-397B-A17B" },
- { id: "qwen/qwen3.5-122b-a10b", name: "Qwen3.5-122B-A10B" },
- { id: "stepfun-ai/step-3.5-flash", name: "Step 3.5 Flash" },
- { id: "stepfun-ai/step-3.7-flash", name: "Step 3.7 Flash" },
- // Sweep 2026-06-19: verified present in the live NVIDIA NIM /v1/models catalog.
- { id: "moonshotai/kimi-k2.6", name: "Kimi K2.6" },
- { id: "openai/gpt-oss-120b", name: "GPT OSS 120B", toolCalling: false },
- { id: "openai/gpt-oss-20b", name: "GPT OSS 20B", toolCalling: false },
+ { id: "google/diffusiongemma-26b-a4b-it", name: "DiffusionGemma 26B A4B IT" },
+ { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra 550B A55B" },
{ id: "nvidia/nemotron-3-super-120b-a12b", name: "Nemotron 3 Super 120B A12B" },
- { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra 550B" },
- // Port of decolua/9router#2373 ("fix(nvidia): expand NIM chat model catalog"):
- // additional live-catalog models observed to serve /v1/chat/completions.
- // `minimaxai/minimax-m3` from that PR is intentionally NOT re-added — it stays
- // excluded per the #3329 guard (nvidia-minimax-m3-removed-3329.test.ts).
- // Non-chat entries from the same PR (nvidia/gliner-pii — NER tagger, not a chat
- // model; google/diffusiongemma-26b-a4b-it — diffusion model) are dropped for the
- // same reason: this registry only models the /v1/chat/completions surface.
- { id: "abacusai/dracarys-llama-3.1-70b-instruct", name: "Dracarys Llama 3.1 70B Instruct" },
- { id: "google/gemma-2-2b-it", name: "Gemma 2 2B IT" },
- { id: "google/gemma-3n-e2b-it", name: "Gemma 3n E2B IT" },
- { id: "meta/llama-3.1-8b-instruct", name: "Llama 3.1 8B Instruct", toolCalling: false },
{
- id: "meta/llama-3.2-11b-vision-instruct",
- name: "Llama 3.2 11B Vision Instruct",
- supportsVision: true,
- },
- { id: "meta/llama-3.2-1b-instruct", name: "Llama 3.2 1B Instruct" },
- { id: "meta/llama-3.2-3b-instruct", name: "Llama 3.2 3B Instruct", toolCalling: false },
- {
- id: "meta/llama-3.2-90b-vision-instruct",
- name: "Llama 3.2 90B Vision Instruct",
- supportsVision: true,
- },
- { id: "meta/llama-4-maverick-17b-128e-instruct", name: "Llama 4 Maverick 17B 128E Instruct" },
- { id: "meta/llama-guard-4-12b", name: "Llama Guard 4 12B", toolCalling: false },
- { id: "mistralai/ministral-14b-instruct-2512", name: "Ministral 14B Instruct 2512" },
- { id: "mistralai/mistral-medium-3.5-128b", name: "Mistral Medium 3.5 128B" },
- { id: "mistralai/mistral-nemotron", name: "Mistral Nemotron" },
- { id: "mistralai/mixtral-8x7b-instruct-v0.1", name: "Mixtral 8x7B Instruct v0.1" },
- {
- id: "nvidia/ising-calibration-1-35b-a3b",
- name: "Ising Calibration 1 35B A3B",
- supportsReasoning: true,
- },
- {
- id: "nvidia/llama-3.1-nemoguard-8b-content-safety",
- name: "Llama 3.1 Nemoguard 8B Content Safety",
- },
- {
- id: "nvidia/llama-3.1-nemoguard-8b-topic-control",
- name: "Llama 3.1 Nemoguard 8B Topic Control",
- },
- { id: "nvidia/llama-3.1-nemotron-nano-8b-v1", name: "Llama 3.1 Nemotron Nano 8B v1" },
- {
- id: "nvidia/llama-3.1-nemotron-nano-vl-8b-v1",
- name: "Llama 3.1 Nemotron Nano VL 8B v1",
- supportsVision: true,
- },
- {
- id: "nvidia/llama-3.1-nemotron-safety-guard-8b-v3",
- name: "Llama 3.1 Nemotron Safety Guard 8B v3",
- },
- { id: "nvidia/llama-3.3-nemotron-super-49b-v1", name: "Llama 3.3 Nemotron Super 49B v1" },
- { id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", name: "Llama 3.3 Nemotron Super 49B v1.5" },
- { id: "nvidia/nemotron-3-content-safety", name: "Nemotron 3 Content Safety" },
- {
- id: "nvidia/nemotron-3-nano-30b-a3b",
- name: "Nemotron 3 Nano 30B A3B",
- supportsReasoning: true,
+ id: "nvidia/nemotron-3.5-lightning-30b-a3b",
+ name: "Nemotron 3.5 Lightning 30B A3B",
},
{
id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
@@ -114,27 +44,6 @@ export const nvidiaProvider: RegistryEntry = {
supportsReasoning: true,
supportsVision: true,
},
- { id: "nvidia/nemotron-3.5-content-safety", name: "Nemotron 3.5 Content Safety" },
- { id: "nvidia/nemotron-mini-4b-instruct", name: "Nemotron Mini 4B Instruct" },
- {
- id: "nvidia/nemotron-nano-12b-v2-vl",
- name: "Nemotron Nano 12B v2 VL",
- supportsReasoning: true,
- supportsVision: true,
- },
- {
- id: "nvidia/nvidia-nemotron-nano-9b-v2",
- name: "NVIDIA Nemotron Nano 9B v2",
- supportsReasoning: true,
- },
- { id: "nvidia/riva-translate-4b-instruct-v1.1", name: "Riva Translate 4B Instruct v1.1" },
- {
- id: "qwen/qwen3-next-80b-a3b-instruct",
- name: "Qwen3 Next 80B A3B Instruct",
- supportsReasoning: true,
- },
- { id: "sarvamai/sarvam-m", name: "Sarvam M" },
- { id: "stockmark/stockmark-2-100b-instruct", name: "Stockmark 2 100B Instruct" },
- { id: "upstage/solar-10.7b-instruct", name: "Solar 10.7B Instruct" },
+ { id: "openai/gpt-oss-120b", name: "GPT OSS 120B", toolCalling: false },
],
};
diff --git a/open-sse/config/providers/registry/seekai/index.ts b/open-sse/config/providers/registry/seekai/index.ts
new file mode 100644
index 0000000000..e37550cc52
--- /dev/null
+++ b/open-sse/config/providers/registry/seekai/index.ts
@@ -0,0 +1,18 @@
+import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
+
+/**
+ * SeekAi (https://seekai.cc) — QuantumNous New-API gateway.
+ * Live-verified 2026-09-02: GET /api/status → system_name=SeekAi,
+ * version=v1.0.0-rc.25, quota_display_type=USD. GET /v1/models is
+ * API-key gated (401 Invalid token without a key). Catalog is dynamic;
+ * no static seed. Referral/aff query params stay out of this entry
+ * (no-hardcoded-referral-codes).
+ */
+export const seekaiProvider = buildOpenAiCompatibleRegistryEntry({
+ id: "seekai",
+ alias: "ska",
+ baseUrl: "https://seekai.cc/v1/chat/completions",
+ modelsUrl: "https://seekai.cc/v1/models",
+ models: [],
+ passthroughModels: true,
+});
diff --git a/open-sse/config/providers/registry/vertex/index.ts b/open-sse/config/providers/registry/vertex/index.ts
index 1eac20fc15..4b72b9f7c4 100644
--- a/open-sse/config/providers/registry/vertex/index.ts
+++ b/open-sse/config/providers/registry/vertex/index.ts
@@ -27,6 +27,7 @@ export const vertexProvider: RegistryEntry = {
{ id: "DeepSeek-V4-Pro", name: "DeepSeek V4 Pro (Vertex Partner)" },
{ id: "Qwen3.6-35B-A3B", name: "Qwen3.6 35B A3B (Vertex Partner)" },
{ id: "GLM-5.1-FP8", name: "GLM-5.1 (Vertex Partner)" },
+ { id: "claude-fable-5-1", name: "Claude Fable 5.1 (Vertex)", targetFormat: "claude" },
{ id: "claude-fable-5", name: "Claude Fable 5 (Vertex)", targetFormat: "claude" },
{ id: "claude-opus-5", name: "Claude Opus 5 (Vertex)", targetFormat: "claude" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5 (Vertex)", targetFormat: "claude" },
diff --git a/open-sse/config/providers/registry/vertex/partner/index.ts b/open-sse/config/providers/registry/vertex/partner/index.ts
index 4cdcd5d0b5..6524cd6a2b 100644
--- a/open-sse/config/providers/registry/vertex/partner/index.ts
+++ b/open-sse/config/providers/registry/vertex/partner/index.ts
@@ -13,6 +13,7 @@ export const vertex_partnerProvider: RegistryEntry = {
{ id: "DeepSeek-V4-Pro", name: "DeepSeek V4 Pro" },
{ id: "Qwen3.6-35B-A3B", name: "Qwen 3.6 35B A3B" },
{ id: "GLM-5.1-FP8", name: "GLM 5.1" },
+ { id: "claude-fable-5-1", name: "Claude Fable 5.1", targetFormat: "claude" },
{ id: "claude-fable-5", name: "Claude Fable 5", targetFormat: "claude" },
{ id: "claude-opus-5", name: "Claude Opus 5", targetFormat: "claude" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", targetFormat: "claude" },
diff --git a/open-sse/config/providers/registry/zai-web/index.ts b/open-sse/config/providers/registry/zai-web/index.ts
index 98901daab9..59b2b61c31 100644
--- a/open-sse/config/providers/registry/zai-web/index.ts
+++ b/open-sse/config/providers/registry/zai-web/index.ts
@@ -14,30 +14,27 @@ export const zai_webProvider: RegistryEntry = {
// Z.ai's visible "Tools" switch enables its internal VLM/MCP tools. It does
// not accept caller-supplied OpenAI `tools`, which remains disabled here.
models: [
+ {
+ id: "glm-5.3-flash",
+ name: "GLM-5.3-Flash",
+ toolCalling: false,
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "high", "max"],
+ supportsVision: true,
+ },
+ {
+ id: "glm-5.3",
+ name: "GLM-5.3",
+ toolCalling: false,
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "high", "max"],
+ },
{
id: "glm-5.2",
name: "GLM-5.2",
toolCalling: false,
supportsReasoning: true,
- },
- {
- id: "GLM-5.1",
- name: "GLM-5.1",
- toolCalling: false,
- supportsReasoning: true,
- },
- {
- id: "GLM-5-Turbo",
- name: "GLM-5-Turbo",
- toolCalling: false,
- supportsReasoning: true,
- },
- {
- id: "GLM-5v-Turbo",
- name: "GLM-5V-Turbo",
- toolCalling: false,
- supportsReasoning: true,
- supportsVision: true,
+ supportedThinkingEfforts: ["high", "max"],
},
],
};
diff --git a/open-sse/executors/adapta-web.ts b/open-sse/executors/adapta-web.ts
index 8c9ae571ea..da1b836002 100644
--- a/open-sse/executors/adapta-web.ts
+++ b/open-sse/executors/adapta-web.ts
@@ -33,6 +33,15 @@ interface CachedSession {
jwtExpiresAt: number; // unix ms
}
+const SESSION_CACHE_MAX = 100;
+
+function evictOldest(cache: Map): void {
+ if (cache.size >= SESSION_CACHE_MAX) {
+ const first = cache.keys().next().value;
+ if (first) cache.delete(first);
+ }
+}
+
// Keyed by the first 32 chars of the stored __client JWT
const sessionCache = new Map();
@@ -44,11 +53,15 @@ function cachedJwt(clientJwt: string): string | null {
const entry = sessionCache.get(cacheKey(clientJwt));
if (!entry) return null;
// Keep a 30-second buffer before expiry
- if (Date.now() >= entry.jwtExpiresAt - 30_000) return null;
+ if (Date.now() >= entry.jwtExpiresAt - 30_000) {
+ sessionCache.delete(cacheKey(clientJwt));
+ return null;
+ }
return entry.jwt;
}
function storeSession(clientJwt: string, sessionId: string, jwt: string, expMs: number): void {
+ evictOldest(sessionCache);
sessionCache.set(cacheKey(clientJwt), { sessionId, jwt, jwtExpiresAt: expMs });
}
diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts
index c16d626812..9582af9fe6 100644
--- a/open-sse/executors/antigravity.ts
+++ b/open-sse/executors/antigravity.ts
@@ -26,6 +26,7 @@ import {
} from "../services/antigravityCredits.ts";
import { persistCreditBalance, getAllPersistedCreditBalances } from "@/lib/db/creditBalance";
import { setConnectionRateLimitUntil } from "@/lib/db/providers";
+import { markAntigravityModelQuotaExhausted } from "../services/antigravityFamilyCooldown.ts";
import { getMitmAlias } from "@/lib/db/models";
import {
MAX_ANTIGRAVITY_OUTPUT_TOKENS,
@@ -245,17 +246,15 @@ export function createCreditsExtractionTransform(
);
}
-/**
- * Persist a quota-exhausted cooldown to the DB for `connectionId` so that
- * cross-request and post-restart routing skips this connection until the
- * cooldown expires. Exported for unit testing. @internal
- */
-export function markConnectionQuotaExhausted(connectionId: string, retryAfterMs: number): void {
+export function markConnectionQuotaExhausted(
+ connectionId: string,
+ retryAfterMs: number,
+ model?: string | null
+): void {
try {
+ if (markAntigravityModelQuotaExhausted(connectionId, retryAfterMs, model)) return;
setConnectionRateLimitUntil(connectionId, Date.now() + retryAfterMs);
- } catch {
- // DB write failure must never crash the request path
- }
+ } catch {}
}
/**
@@ -1620,7 +1619,7 @@ export class AntigravityExecutor extends BaseExecutor {
updateAntigravityRemainingCredits
);
if (creditsResult) return { kind: "return", result: creditsResult };
- if (retryMs) markConnectionQuotaExhausted(accountId, retryMs);
+ if (retryMs) markConnectionQuotaExhausted(accountId, retryMs, ctx.model);
}
return {
diff --git a/open-sse/executors/antigravity/sseCollect.ts b/open-sse/executors/antigravity/sseCollect.ts
index 630b091aab..5b7ef3ea85 100644
--- a/open-sse/executors/antigravity/sseCollect.ts
+++ b/open-sse/executors/antigravity/sseCollect.ts
@@ -16,6 +16,11 @@ export type AntigravityCollectedStream = {
remainingCredits: Array<{ creditType: string; creditAmount: string }> | null;
};
+// Both run once per SSE data line / per text part (processAntigravitySSEPayload),
+// so the literals are hoisted to module constants.
+const TEXTUAL_TOOL_CALL_RE =
+ /^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/;
+
export function stripZeroWidth(value: unknown): unknown {
if (typeof value === "string") {
return stripObfuscationZeroWidth(value);
@@ -39,9 +44,7 @@ export function parseAntigravityTextualToolCall(
): { name: string; args: unknown } | null {
if (typeof text !== "string") return null;
const normalized = stripObfuscationZeroWidth(text);
- const match = normalized.match(
- /^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/
- );
+ const match = normalized.match(TEXTUAL_TOOL_CALL_RE);
if (!match) return null;
const name = match[1]?.trim();
const rawArgs = match[2]?.trim();
diff --git a/open-sse/executors/cloudflare-playground.ts b/open-sse/executors/cloudflare-playground.ts
index ba309f1eed..249e069da3 100644
--- a/open-sse/executors/cloudflare-playground.ts
+++ b/open-sse/executors/cloudflare-playground.ts
@@ -36,6 +36,7 @@
import { randomUUID } from "crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
+import { connectObscuraBrowser } from "../services/obscura.ts";
import type { Browser, Page } from "playwright";
export const PLAYGROUND_URL = "https://playground.ai.cloudflare.com/";
@@ -296,14 +297,22 @@ export class PlaywrightCfTransport implements CfTransport {
config: CfTransportConfig
): Promise<{ ok: true } | { ok: false; status: number; message: string }> {
try {
+ // #12274: prefer the shared Obscura browser (browser-grade TLS fingerprint
+ // on the WS upgrade, ~30MB) over a full Chromium per request; fall back to
+ // a direct Chromium launch when Obscura is unavailable.
+ const obscura = await connectObscuraBrowser();
const playwright = await importPlaywright();
- const executablePath =
- this.chromeExecutablePath ?? process.env.CLOUDFLARE_PLAYGROUND_CHROME_PATH;
- this.browser = await playwright.chromium.launch({
- ...(executablePath ? { executablePath } : {}),
- headless: true,
- args: BROWSER_ARGS,
- });
+ if (obscura) {
+ this.browser = obscura.browser;
+ } else {
+ const executablePath =
+ this.chromeExecutablePath ?? process.env.CLOUDFLARE_PLAYGROUND_CHROME_PATH;
+ this.browser = await playwright.chromium.launch({
+ ...(executablePath ? { executablePath } : {}),
+ headless: true,
+ args: BROWSER_ARGS,
+ });
+ }
const context = await this.browser.newContext({ userAgent: PLAYGROUND_UA });
const page = await context.newPage();
this.page = page;
diff --git a/open-sse/executors/clova-studio.ts b/open-sse/executors/clova-studio.ts
new file mode 100644
index 0000000000..28997a78b9
--- /dev/null
+++ b/open-sse/executors/clova-studio.ts
@@ -0,0 +1,12 @@
+import { DefaultExecutor } from "./default.ts";
+
+/** CLOVA Chat Completions v3 places the selected model in the URL path. */
+export class ClovaStudioExecutor extends DefaultExecutor {
+ constructor() {
+ super("clova-studio");
+ }
+
+ buildUrl(model: string): string {
+ return `${this.config.baseUrl}/${encodeURIComponent(model)}`;
+ }
+}
diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts
index 62e3a1da13..4701f175c2 100644
--- a/open-sse/executors/codex.ts
+++ b/open-sse/executors/codex.ts
@@ -538,6 +538,10 @@ export function codexDropNonstandardEvents(): boolean {
// every `codex.*` event block from the byte stream before it reaches the client.
// Exported for unit testing (#4715). Strips `codex.*` SSE event blocks from a
// streaming Response when `codexDropNonstandardEvents()` is on (default, #11014).
+// Pre-compiled: the filter's transform() runs on every chunk, so these were
+// re-allocated per block/iteration before hoisting.
+const CODEX_SSE_EVENT_LINE_RE = /^event:\s*(.+)$/m;
+const CODEX_SSE_BLOCK_SEP_RE = /\r?\n\r?\n/;
export function filterNonstandardCodexSse(response: Response): Response {
const contentType = response.headers.get("content-type") || "";
if (!response.body || !contentType.includes("text/event-stream")) {
@@ -547,14 +551,14 @@ export function filterNonstandardCodexSse(response: Response): Response {
const encoder = new TextEncoder();
let buffer = "";
const dropBlock = (block: string): boolean => {
- const match = /^event:\s*(.+)$/m.exec(block);
+ const match = CODEX_SSE_EVENT_LINE_RE.exec(block);
return !!match && match[1].trim().startsWith("codex.");
};
const transform = new TransformStream({
transform(chunk, controller) {
buffer += decoder.decode(chunk, { stream: true });
while (true) {
- const separator = /\r?\n\r?\n/.exec(buffer);
+ const separator = CODEX_SSE_BLOCK_SEP_RE.exec(buffer);
if (!separator) break;
const blockEnd = separator.index + separator[0].length;
const block = buffer.slice(0, blockEnd);
diff --git a/open-sse/executors/glm.ts b/open-sse/executors/glm.ts
index 7f1b850b23..ef9f370669 100644
--- a/open-sse/executors/glm.ts
+++ b/open-sse/executors/glm.ts
@@ -223,6 +223,8 @@ export function translateSseResponse(
suppressThinkClose: boolean = false
): Response {
if (!response.body) return response;
+ // GLM is a high-throughput provider — use a larger stream buffer (64KB) to
+ // keep provider → client pacing ahead of the model's token emission rate.
const transform = createSSETransformStreamWithLogger(
FORMATS.CLAUDE,
FORMATS.OPENAI,
@@ -236,7 +238,10 @@ export function translateSseResponse(
null,
null,
false,
- suppressThinkClose
+ suppressThinkClose,
+ undefined,
+ undefined,
+ 65536
);
const headers = cloneHeaders(response.headers);
headers.set("content-type", "text/event-stream");
diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts
index d1c3b9ced0..fed0a651c1 100644
--- a/open-sse/executors/index.ts
+++ b/open-sse/executors/index.ts
@@ -180,6 +180,7 @@ const lazyExecutors: Record Promise> = {
xai: () => import("./xai.ts").then((m) => new m.XaiExecutor()),
"xai-oauth": () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")),
xao: () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")),
+ "clova-studio": () => import("./clova-studio.ts").then((m) => new m.ClovaStudioExecutor()),
"conol-web": () => import("./conol-web.ts").then((m) => new m.ConolWebExecutor()),
cnl: () => import("./conol-web.ts").then((m) => new m.ConolWebExecutor()), // Alias
};
diff --git a/open-sse/executors/tinycms.ts b/open-sse/executors/tinycms.ts
index 68c916e047..c100f76e38 100644
--- a/open-sse/executors/tinycms.ts
+++ b/open-sse/executors/tinycms.ts
@@ -16,7 +16,9 @@ async function getPublicIp(): Promise {
return publicIp;
}
try {
- const res = await fetch("https://api64.ipify.org?format=json");
+ const res = await fetch("https://api64.ipify.org?format=json", {
+ signal: AbortSignal.timeout(5000),
+ });
const json = (await res.json()) as { ip: string };
publicIp = json.ip;
lastIpFetch = now;
@@ -35,6 +37,7 @@ async function fetchChallenge(uuid: string): Promise {
Accept: "application/json",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
},
+ signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
throw new Error(`Failed to fetch challenge: ${res.status}`);
diff --git a/open-sse/executors/zai-web.ts b/open-sse/executors/zai-web.ts
index 6e01aee98d..fb09e4ea82 100644
--- a/open-sse/executors/zai-web.ts
+++ b/open-sse/executors/zai-web.ts
@@ -5,8 +5,9 @@
* browser-issued CAPTCHA proof for chat completions. The browser transport is
* the default; callers with a short-lived proof can use the direct HTTP path.
*
- * Completions go to /api/v2/chat/completions; the older unversioned
- * /api/chat/completions path is stale and 404s model-independently (#8014).
+ * Completions go to /api/v2/chat/completions. Z.ai's CAPTCHA rejects true
+ * headless Chromium with F001, so the browser transport uses off-screen headed
+ * Chromium while retaining the shared browser pool.
*/
import { createHash, randomUUID } from "node:crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
@@ -67,6 +68,7 @@ export {
parseZaiFrontendVersion,
resolveZaiThinkingConfig,
resolveZaiVlmConfig,
+ zaiUpstreamModelId,
} from "./zai-web/protocol.ts";
export type {
ZaiModelCapabilities,
@@ -174,6 +176,7 @@ function buildZaiBrowserChatOptions(input: {
userAgent: ZAI_USER_AGENT,
locale: "en-US",
timezone: "Asia/Seoul",
+ headless: false,
inputSelector: "#chat-input",
submitButtonSelector: '[aria-label="Send Message"] button:not([disabled])',
submitButtonMode: "dom",
@@ -243,7 +246,7 @@ function resolveZaiRequest(
const modelId = (bodyObj.model as string) || model || ZAI_DEFAULT_MODEL;
if (imageUrls.length > 0 && !getZaiModelCapabilities(modelId).vision) {
return fail(
- `Z.ai model ${unprefixedModelId(modelId)} does not accept image input; use GLM-5V-Turbo.`
+ `Z.ai model ${unprefixedModelId(modelId)} does not accept image input; use GLM-5.3-Flash.`
);
}
diff --git a/open-sse/executors/zai-web/browserAutomation.ts b/open-sse/executors/zai-web/browserAutomation.ts
index 38924ada4d..a8aabccc33 100644
--- a/open-sse/executors/zai-web/browserAutomation.ts
+++ b/open-sse/executors/zai-web/browserAutomation.ts
@@ -23,14 +23,17 @@ async function runStage(name: string, action: () => Promise): Promise {
const selector = page.locator('[aria-label="Select a model"]').first();
await selector.waitFor({ state: "visible", timeout: 10_000 });
- if ((await selector.innerText()).includes(modelName)) return;
+ if ((await selector.getByText(modelName, { exact: true }).count()) > 0) return;
// The landing-page hero animation can remain above the already-visible
// selector and make coordinate-based clicks time out.
await selector.evaluate((element) => (element as HTMLElement).click());
const menu = page.locator('[role="menu"]').filter({ hasText: modelName }).first();
await menu.waitFor({ state: "visible", timeout: 5_000 });
- const modelButton = menu.locator("button").filter({ hasText: modelName }).first();
+ const modelButton = menu
+ .getByText(modelName, { exact: true })
+ .first()
+ .locator("xpath=ancestor::button[1]");
await modelButton.evaluate((element) => (element as HTMLElement).click());
await page
.locator('[aria-label="Select a model"]')
@@ -73,13 +76,13 @@ async function setZaiBrowserWebSearch(page: Page, enabled: boolean): Promise,
effort: ZaiThinkingConfig["effort"]
): Promise {
const effortButton = menu.locator("button").filter({
- hasText: effort === "high" ? "High" : "Max",
+ hasText: effort === "low" ? "Low" : effort === "high" ? "High" : "Max",
});
if ((await effortButton.getAttribute("data-selected")) === "true") return;
await runStage(`select ${effort}`, () =>
diff --git a/open-sse/executors/zai-web/protocol.ts b/open-sse/executors/zai-web/protocol.ts
index 562ca85a7f..511e5d3aa9 100644
--- a/open-sse/executors/zai-web/protocol.ts
+++ b/open-sse/executors/zai-web/protocol.ts
@@ -7,8 +7,8 @@ import { normalizeCookie, sanitizeErrorMessage } from "../../utils/error.ts";
export const ZAI_BASE_URL = "https://chat.z.ai";
export const ZAI_NEW_CHAT_URL = `${ZAI_BASE_URL}/api/v1/chats/new`;
export const ZAI_CHAT_URL = `${ZAI_BASE_URL}/api/v2/chat/completions`;
-export const ZAI_DEFAULT_MODEL = "GLM-5.1";
-export const ZAI_DEFAULT_FE_VERSION = "prod-fe-1.1.79";
+export const ZAI_DEFAULT_MODEL = "glm-5.3";
+export const ZAI_DEFAULT_FE_VERSION = "prod-fe-1.1.92";
export const ZAI_USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
export const ZAI_FE_VERSION_CACHE_TTL_MS = 15 * 60 * 1000;
@@ -21,7 +21,7 @@ export interface NewChatRequest {
userMessageId: string;
}
-export type ZaiReasoningEffort = "high" | "max";
+export type ZaiReasoningEffort = "low" | "high" | "max";
export interface ZaiThinkingConfig {
enabled: boolean;
@@ -61,12 +61,23 @@ const NO_ZAI_MODEL_CAPABILITIES: ZaiModelCapabilities = Object.freeze({
});
/**
- * Verified against chat.z.ai/api/models (prod-fe-1.1.79).
+ * Verified against chat.z.ai/api/models (prod-fe-1.1.92).
* `returnFc` is the site's internal function-call result capability; it is
* distinct from accepting caller-supplied OpenAI `tools`.
*/
const ZAI_MODEL_CAPABILITIES: Record = {
- "glm-5.2": {
+ "glm-5.3-flash": {
+ mcp: false,
+ reasoningEffort: true,
+ returnFc: true,
+ thinking: true,
+ vision: true,
+ vlmTools: false,
+ vlmWebSearch: false,
+ vlmWebsiteMode: false,
+ webSearch: true,
+ },
+ "glm-5.3": {
mcp: true,
reasoningEffort: true,
returnFc: true,
@@ -77,9 +88,9 @@ const ZAI_MODEL_CAPABILITIES: Record = {
vlmWebsiteMode: false,
webSearch: true,
},
- "glm-5.1": {
+ "glm-5.2": {
mcp: true,
- reasoningEffort: false,
+ reasoningEffort: true,
returnFc: true,
thinking: true,
vision: false,
@@ -88,28 +99,6 @@ const ZAI_MODEL_CAPABILITIES: Record = {
vlmWebsiteMode: false,
webSearch: true,
},
- "glm-5-turbo": {
- mcp: true,
- reasoningEffort: false,
- returnFc: true,
- thinking: true,
- vision: false,
- vlmTools: false,
- vlmWebSearch: false,
- vlmWebsiteMode: false,
- webSearch: true,
- },
- "glm-5v-turbo": {
- mcp: false,
- reasoningEffort: false,
- returnFc: true,
- thinking: true,
- vision: true,
- vlmTools: true,
- vlmWebSearch: true,
- vlmWebsiteMode: true,
- webSearch: true,
- },
};
export function asRecord(value: unknown): Record | null {
@@ -136,6 +125,7 @@ export function describeZaiBrowserFailure(result: {
status: number;
body: Buffer;
observedPostUrls?: string[];
+ observedPostResponses?: Array<{ url: string; status: number }>;
timing: { captureResponseMs: number; totalMs: number };
}): string {
const status = result.status > 0 ? String(result.status) : "no matching response";
@@ -144,10 +134,16 @@ export function describeZaiBrowserFailure(result: {
result.observedPostUrls && result.observedPostUrls.length > 0
? ` Observed POST targets: ${result.observedPostUrls.join(", ")}.`
: "";
+ const observedResponses =
+ result.observedPostResponses && result.observedPostResponses.length > 0
+ ? ` Observed POST responses: ${result.observedPostResponses
+ .map(({ url, status }) => `${url} [${status}]`)
+ .join(", ")}.`
+ : "";
const detail =
browserFailureDetail(result.body) ||
(result.status === 0
- ? `The page did not issue the expected authenticated chat completion request.${observed}`
+ ? `The page did not issue the expected authenticated chat completion request.${observed}${observedResponses}`
: "The browser response body was empty.");
return `Z.ai browser transport failed (${status}; ${timing}): ${detail}`;
}
@@ -307,17 +303,27 @@ export function unprefixedModelId(modelId: string): string {
return modelId.trim().split("/").at(-1) || modelId.trim();
}
-export function browserModelName(modelId: string): string {
+/** Map OmniRoute's public Flash id to the opaque id used by chat.z.ai's wire API. */
+export function zaiUpstreamModelId(modelId: string): string {
const unprefixed = unprefixedModelId(modelId);
- if (unprefixed.toLowerCase() === "glm-5.2") return "GLM-5.2";
- if (unprefixed.toLowerCase() === "glm-5v-turbo") return "GLM-5V-Turbo";
- return unprefixed;
+ return unprefixed.toLowerCase() === "glm-5.3-flash" ? "x-preview-l" : unprefixed;
+}
+
+function zaiCapabilityModelId(modelId: string): string {
+ const unprefixed = unprefixedModelId(modelId).toLowerCase();
+ return unprefixed === "x-preview-l" ? "glm-5.3-flash" : unprefixed;
+}
+
+export function browserModelName(modelId: string): string {
+ const normalized = zaiCapabilityModelId(modelId);
+ if (normalized === "glm-5.3-flash") return "GLM-5.3-Flash";
+ if (normalized === "glm-5.3") return "GLM-5.3";
+ if (normalized === "glm-5.2") return "GLM-5.2";
+ return unprefixedModelId(modelId);
}
export function getZaiModelCapabilities(modelId: string): ZaiModelCapabilities {
- return (
- ZAI_MODEL_CAPABILITIES[unprefixedModelId(modelId).toLowerCase()] ?? NO_ZAI_MODEL_CAPABILITIES
- );
+ return ZAI_MODEL_CAPABILITIES[zaiCapabilityModelId(modelId)] ?? NO_ZAI_MODEL_CAPABILITIES;
}
function getFeatureOption(body: Record, key: string): unknown {
@@ -325,7 +331,7 @@ function getFeatureOption(body: Record, key: string): unknown {
return asRecord(body.features)?.[key];
}
-/** Resolve each model's Deep Think control; only GLM-5.2 accepts High/Max effort. */
+/** Resolve each model's Deep Think control using its currently exposed effort vocabulary. */
export function resolveZaiThinkingConfig(
modelId: string,
body: Record
@@ -339,19 +345,26 @@ export function resolveZaiThinkingConfig(
: typeof reasoning?.effort === "string"
? reasoning.effort.trim().toLowerCase()
: "";
- const disabled = body.enable_thinking === false || rawEffort === "none" || rawEffort === "off";
+ const supportsLowEffort = zaiCapabilityModelId(modelId) !== "glm-5.2";
const effort: ZaiReasoningEffort =
- rawEffort === "low" || rawEffort === "medium" || rawEffort === "high" ? "high" : "max";
+ rawEffort === "low" && supportsLowEffort
+ ? "low"
+ : rawEffort === "low" || rawEffort === "medium" || rawEffort === "high"
+ ? "high"
+ : "max";
return {
supported,
- enabled: supported && !disabled,
+ // The current GLM-5.3/5.2 consumer models expose effort selection but no
+ // non-thinking mode. Keep Deep Think enabled even when a generic client
+ // sends an off/none compatibility value.
+ enabled: supported,
effort,
effortSupported: capabilities.reasoningEffort,
};
}
-/** Resolve GLM-5V-Turbo's visible Web Search and Tools controls. */
+/** Resolve the selected model's visible Web Search and Tools controls. */
export function resolveZaiVlmConfig(modelId: string, body: Record): ZaiVlmConfig {
const capabilities = getZaiModelCapabilities(modelId);
const toolsOption = getFeatureOption(body, "vlm_tools_enable");
@@ -425,7 +438,7 @@ export function buildZaiCompletionUrl(input: {
hostname: "chat.z.ai",
protocol: "https:",
referrer: "",
- title: "Z.ai - Advanced AI Chatbot & Agent powered by GLM-5.2",
+ title: "Z.ai - Advanced AI Chatbot & Agent powered by GLM-5.3",
timezone_offset: "0",
local_time: now.toISOString(),
utc_time: now.toUTCString(),
@@ -448,13 +461,14 @@ export function buildZaiNewChatBody(
): NewChatRequest {
const prompt = latestUserPrompt(messages);
const userMessageId = randomUUID();
+ const upstreamModelId = zaiUpstreamModelId(modelId);
return {
userMessageId,
payload: {
chat: {
id: "",
title: "New Chat",
- models: [modelId],
+ models: [upstreamModelId],
params: {},
history: {
messages: {
@@ -465,7 +479,7 @@ export function buildZaiNewChatBody(
role: "user",
content: prompt,
timestamp: Math.floor(Date.now() / 1000),
- models: [modelId],
+ models: [upstreamModelId],
},
},
currentId: userMessageId,
@@ -530,7 +544,7 @@ export function buildZaiRequestBody(input: {
}
return {
stream: true,
- model: input.modelId,
+ model: zaiUpstreamModelId(input.modelId),
messages: foldMessages(input.messages),
signature_prompt: input.prompt,
params,
diff --git a/open-sse/executors/zcodeProtocol.ts b/open-sse/executors/zcodeProtocol.ts
index 12a5cd1a0e..b787b73d4f 100644
--- a/open-sse/executors/zcodeProtocol.ts
+++ b/open-sse/executors/zcodeProtocol.ts
@@ -178,7 +178,7 @@ export class ZcodeAppServerClient implements ZcodeClientLike {
private readonly startupTimeoutMs: number;
private readonly requestTimeoutMs: number;
private child?: ChildProcessWithoutNullStreams;
- private outputBuffer = Buffer.alloc(0);
+ private pendingChunks: Buffer[] = [];
private handshakeDone = false;
private ready = false;
private startPromise?: Promise;
@@ -220,7 +220,7 @@ export class ZcodeAppServerClient implements ZcodeClientLike {
}
this.child = child;
- this.outputBuffer = Buffer.alloc(0);
+ this.pendingChunks = [];
this.handshakeDone = false;
this.ready = false;
child.stdin.on("error", () => {
@@ -271,18 +271,29 @@ export class ZcodeAppServerClient implements ZcodeClientLike {
}
}
+ // Buffer accumulated stdout bytes. Chunks are collected in an array and
+ // collapsed into one contiguous buffer only when a complete frame (or the
+ // hello line) might be present — the previous `concat(prev, chunk)` per data
+ // event re-allocated the whole buffer on every chunk, i.e. O(n²) total.
private onStdout(chunk: Buffer): void {
- this.outputBuffer = Buffer.concat([this.outputBuffer, chunk]);
+ this.pendingChunks.push(chunk);
+ let total = 0;
+ for (const part of this.pendingChunks) total += part.byteLength;
+ const buffer = total === chunk.byteLength && this.pendingChunks.length > 0
+ ? chunk
+ : Buffer.concat(this.pendingChunks);
+ this.pendingChunks = [buffer];
+
if (!this.handshakeDone) {
- const newline = this.outputBuffer.indexOf(0x0a);
+ const newline = buffer.indexOf(0x0a);
if (newline < 0) {
- if (this.outputBuffer.byteLength > 64 * 1024) {
+ if (buffer.byteLength > 64 * 1024) {
this.serverReadyError?.(new Error("ZCode hello line is too large"));
}
return;
}
- const line = this.outputBuffer.subarray(0, newline).toString("utf8").trim();
- this.outputBuffer = this.outputBuffer.subarray(newline + 1);
+ const line = buffer.subarray(0, newline).toString("utf8").trim();
+ this.pendingChunks = [buffer.subarray(newline + 1)];
let hello: unknown;
try {
hello = JSON.parse(line);
@@ -307,9 +318,13 @@ export class ZcodeAppServerClient implements ZcodeClientLike {
}
private consumeFrames(): void {
- while (this.outputBuffer.byteLength >= HEADER_SIZE) {
- const type = this.outputBuffer.readUInt8(0);
- const length = this.outputBuffer.readUInt32BE(9);
+ // Collapse to one buffer for frame scanning (only happens once per data
+ // event since onStdout already deduped), then drop consumed frames.
+ const buffer = this.pendingChunks[0];
+ let offset = 0;
+ while (buffer.byteLength - offset >= HEADER_SIZE) {
+ const type = buffer.readUInt8(offset);
+ const length = buffer.readUInt32BE(offset + 9);
if (length > MAX_FRAME_BYTES) {
const error = new Error("ZCode frame exceeds the configured safety limit");
this.serverReadyError?.(error);
@@ -317,9 +332,9 @@ export class ZcodeAppServerClient implements ZcodeClientLike {
return;
}
const frameLength = HEADER_SIZE + length;
- if (this.outputBuffer.byteLength < frameLength) return;
- const body = this.outputBuffer.subarray(HEADER_SIZE, frameLength);
- this.outputBuffer = this.outputBuffer.subarray(frameLength);
+ if (buffer.byteLength - offset < frameLength) break;
+ const body = buffer.subarray(offset + HEADER_SIZE, offset + frameLength);
+ offset += frameLength;
if (type !== REGULAR_MESSAGE) continue;
try {
const header = decodeZcodeValue(body, 0);
@@ -331,6 +346,7 @@ export class ZcodeAppServerClient implements ZcodeClientLike {
this.rejectPending(normalized);
}
}
+ if (offset > 0) this.pendingChunks = [buffer.subarray(offset)];
}
private handleMessage(headerValue: unknown, payload: unknown): void {
diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts
index 8dbac0018b..65a986d186 100644
--- a/open-sse/handlers/chatCore.ts
+++ b/open-sse/handlers/chatCore.ts
@@ -360,6 +360,10 @@ import { deleteSessionAccountAffinity } from "@/lib/db/sessionAccountAffinity";
import { getCacheControlSettings } from "@/lib/cacheControlSettings";
import { guardrailRegistry } from "@/lib/guardrails";
import type { VideoBridgeLogRedactionEntry } from "@/lib/guardrails/videoBridge";
+import {
+ logClientRawRequestRedacted,
+ redactPendingBody,
+} from "@/lib/guardrails/videoBridgeSnapshotRedaction";
import {
shouldPreserveCacheControl,
resolveConnectionCacheOverride,
@@ -380,6 +384,7 @@ import { sanitizeOpenAITool } from "../services/toolSchemaSanitizer.ts";
import { isCompactResponsesEndpoint } from "../executors/codex.ts";
import { persistCodexChildQuotaResponse } from "../services/codexAccount/index.ts";
import { invalidateCodexQuotaCache } from "../services/codexQuotaFetcher.ts";
+import { invalidateGenericQuotaCacheOnStatus } from "../services/genericQuotaFetcher.ts";
import { translateNonStreamingResponse } from "./responseTranslator.ts";
import { extractToolSchemaMap } from "../translator/response/openai-responses/toolSchemas.ts";
import { unwrapClineNonStreamingEnvelope } from "./chatCore/clineResponseEnvelope.ts";
@@ -927,7 +932,7 @@ export async function handleChatCore({
const pendingRequestId =
trackPendingRequest(model, provider, pendingConnId, true, {
clientEndpoint: clientRawRequest?.endpoint || "/v1/chat/completions",
- clientRequest: clientRawRequest?.body ?? body,
+ clientRequest: redactPendingBody(clientRawRequest?.body ?? body, videoBridgeObserved),
providerRequest: initialProviderRequest,
stage: "registered",
correlationId,
@@ -1211,14 +1216,9 @@ export async function handleChatCore({
});
const pendingScope = { id: pendingRequestId, model, provider, connectionId: pendingConnId };
const providerRequestCapture = createPreparedRequestLogger(reqLogger, pendingScope);
- // 0. Log client raw request (before format conversion)
- if (clientRawRequest) {
- reqLogger.logClientRawRequest(
- clientRawRequest.endpoint,
- clientRawRequest.body,
- clientRawRequest.headers
- );
- }
+ // 0. Log client raw request (before format conversion) — redacts video transcript
+ // cues in the logged copy only; see videoBridgeSnapshotRedaction.ts.
+ logClientRawRequestRedacted(reqLogger, clientRawRequest, videoBridgeObserved);
const reasoningRouteDecision =
body && typeof body === "object"
? (body as Record)._omnirouteReasoningRouteTrace
@@ -3240,6 +3240,14 @@ export async function handleChatCore({
const errMessage = err instanceof Error ? err.message : String(err);
log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
}
+ } else if (attemptConnectionId && res.response.status === 429) {
+ // Dropped generic quota cache after 429
+ invalidateGenericQuotaCacheOnStatus({
+ provider,
+ connectionId: String(attemptConnectionId),
+ status: res.response.status,
+ isolateProbe: await shouldIsolateProbeFailures(),
+ });
}
// Track Gemini RPM + RPD request counts for 429 classification
diff --git a/open-sse/handlers/chatCore/attemptLogging.ts b/open-sse/handlers/chatCore/attemptLogging.ts
index 2a583896a7..07de9f19b2 100644
--- a/open-sse/handlers/chatCore/attemptLogging.ts
+++ b/open-sse/handlers/chatCore/attemptLogging.ts
@@ -13,6 +13,7 @@
import { extractProviderWarnings } from "@/lib/compliance/providerAudit";
import { logAuditEvent } from "@/lib/compliance";
import { emit } from "@/lib/events/eventBus";
+import { maybeLogToolCallSpecViolation } from "./toolCallSpecViolationAudit.ts";
import type { RequestCompletedPayload, RequestFailedPayload } from "@/lib/events/types";
import { saveCallLog } from "@/lib/usageDb";
import type { VideoBridgeLogRedactionEntry } from "@/lib/guardrails/videoBridge";
@@ -50,6 +51,16 @@ import { attachLogMeta } from "./cacheUsageMeta.ts";
* never touches a part whose text differs — see
* `tests/unit/video-bridge-log-redaction.test.ts`'s "Scenario A" test for the
* reproduction this fixes.
+ *
+ * #12430 item 4 (P2c): a message's `content` can also be a plain STRING that
+ * embeds `fullText` as a SUBSTRING rather than an exact array part — derived
+ * dispatches (pipeline-strategy stages, smart-auto-pipeline, context-handoff
+ * summaries) all interpolate the transcript blob into a larger rendered
+ * prompt string before calling `handleSingleModel`. That string branch is
+ * mutually exclusive with the array branch (a message's `content` is one or
+ * the other, never both) and uses `String.prototype.replaceAll` against the
+ * trusted `fullText` literal to swap every occurrence — see
+ * `tests/unit/video-bridge-derived-prompt-redaction.test.ts`.
*/
export function applyVideoBridgeLogRedaction(
body: unknown,
@@ -78,6 +89,47 @@ export function applyVideoBridgeLogRedaction(
const originalMessage = originalContainer[messageIndex];
if (!originalMessage || typeof originalMessage !== "object") continue;
const originalContent = (originalMessage as Record).content;
+
+ // Derived-prompt dispatches (pipeline-strategy stages, smart-auto-pipeline,
+ // context-handoff summaries — #12430 item 4) embed the transcript as a
+ // SUBSTRING of a plain string `content`, e.g. a rendered stage prompt or a
+ // `{HISTORY}`-interpolated handoff summary, never as an exact array part.
+ // Mutually exclusive with the array branch below: a message's `content`
+ // is either a string or an array, never both, so this and the
+ // `Array.isArray` check never both match the same message.
+ if (typeof originalContent === "string") {
+ if (!originalContent.includes(fullText)) continue;
+
+ // Same lazy clone-on-write as the array branch: root -> container
+ // array -> this message. Siblings keep referencing the originals.
+ if (!rootClone) rootClone = { ...source };
+ let containerClone = clonedContainers.get(container);
+ if (!containerClone) {
+ containerClone = [...originalContainer];
+ clonedContainers.set(container, containerClone);
+ rootClone[container] = containerClone;
+ }
+
+ const messageKey = `${container}:${messageIndex}`;
+ let messageClone = clonedMessages.get(messageKey);
+ if (!messageClone) {
+ messageClone = { ...(originalMessage as Record) };
+ clonedMessages.set(messageKey, messageClone);
+ containerClone[messageIndex] = messageClone;
+ }
+
+ // Re-read from the (possibly already-cloned) message so a second
+ // redaction entry matching the same string content composes with the
+ // first instead of clobbering it. `fullText` is a trusted literal
+ // (the `[Video description:...]` blob), so replaceAll(string, string)
+ // needs no regex and is safe. replaceAll (not replace): a stage/summary
+ // prompt can quote the transcript back more than once.
+ const currentText =
+ typeof messageClone.content === "string" ? messageClone.content : originalContent;
+ messageClone.content = currentText.replaceAll(fullText, redactedText);
+ redacted = true;
+ continue;
+ }
if (!Array.isArray(originalContent)) continue;
for (let partIndex = 0; partIndex < originalContent.length; partIndex++) {
@@ -341,6 +393,15 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
});
}
+ maybeLogToolCallSpecViolation({
+ responseBody,
+ provider,
+ model,
+ connectionId: finalConnectionId,
+ httpStatus: status,
+ requestId: skillRequestId,
+ });
+
const capturedPipeline = reqLogger?.getPipelinePayloads?.() ?? null;
const pipelinePayloads = detailedLoggingEnabled
? (capturedPipeline ?? {})
diff --git a/open-sse/handlers/chatCore/toolCallSpecViolationAudit.ts b/open-sse/handlers/chatCore/toolCallSpecViolationAudit.ts
new file mode 100644
index 0000000000..573daae59e
--- /dev/null
+++ b/open-sse/handlers/chatCore/toolCallSpecViolationAudit.ts
@@ -0,0 +1,46 @@
+/**
+ * Post-request on-spec audit for duplicated tool_calls.
+ *
+ * Extracted from persistAttemptLogs so attemptLogging.ts stays at the frozen
+ * complexity count. validateResponseQuality's streaming peek only sees the
+ * START of a stream, so a duplicate that arrives after real content has
+ * already been relayed cannot fail the attempt over — this is the first
+ * point the fully assembled body is available. Too late to retry; a durable
+ * audit row still beats a clean HTTP 200 with no trace.
+ *
+ * Observed: minimax-m3:free via OpenRouter/GMICloud, 2026-09-02, duplicated
+ * a heartbeat_respond call byte-for-byte.
+ */
+
+import { logAuditEvent } from "@/lib/compliance";
+import { findToolCallSpecViolation } from "../../services/combo/validateQuality.ts";
+
+export function maybeLogToolCallSpecViolation(input: {
+ responseBody: unknown;
+ provider: string | null | undefined;
+ model: string | null | undefined;
+ connectionId: string | null;
+ httpStatus: number;
+ requestId: string;
+}): void {
+ const violation = findToolCallSpecViolation(input.responseBody);
+ if (!violation) return;
+ logAuditEvent({
+ action: "provider.spec_violation",
+ actor: "system",
+ target:
+ [input.provider, input.connectionId].filter(Boolean).join(":") ||
+ input.provider ||
+ input.model,
+ resourceType: "provider_spec_violation",
+ status: "warning",
+ requestId: input.requestId,
+ details: {
+ provider: input.provider,
+ model: input.model,
+ connectionId: input.connectionId,
+ httpStatus: input.httpStatus,
+ violation,
+ },
+ });
+}
diff --git a/open-sse/handlers/embeddingStructuredInput.ts b/open-sse/handlers/embeddingStructuredInput.ts
index 1183c9e5a3..1cd7571a38 100644
--- a/open-sse/handlers/embeddingStructuredInput.ts
+++ b/open-sse/handlers/embeddingStructuredInput.ts
@@ -128,10 +128,7 @@ export async function prepareJinaMixedEmbeddingInput(
continue;
}
if (isCanonicalEmbeddingItem(item)) {
- const [translated] = await prepareJinaInput(
- [item as EmbeddingMultimodalItem],
- fetchMedia
- );
+ const [translated] = await prepareJinaInput([item as EmbeddingMultimodalItem], fetchMedia);
out.push(translated);
continue;
}
@@ -163,7 +160,9 @@ function embeddingValues(entry: unknown): unknown[] {
return Array.isArray(values) ? values : [];
}
-function normalizeGeminiEmbedContentResponse(data: Record): Record {
+function normalizeGeminiEmbedContentResponse(
+ data: Record
+): Record {
return {
object: "list",
data: [{ object: "embedding", embedding: embeddingValues(data.embedding), index: 0 }],
@@ -263,10 +262,7 @@ async function itemToGeminiContent(
return { parts: [await jinaDocToGeminiPart(item, fetchMedia)] };
}
if (isCanonicalEmbeddingItem(item)) {
- const [part] = await prepareGeminiParts(
- [item as EmbeddingMultimodalItem],
- fetchMedia
- );
+ const [part] = await prepareGeminiParts([item as EmbeddingMultimodalItem], fetchMedia);
return { parts: [part] };
}
throw new Error("Unsupported Gemini embedding input item");
@@ -346,3 +342,41 @@ export async function prepareStructuredEmbeddingRequest(
}
throw new Error(`Provider ${provider.id} has no structured embedding input translator`);
}
+
+/**
+ * Normalize a single-text embedding endpoint's response into OpenAI's
+ * `/v1/embeddings` list shape.
+ *
+ * CLOVA Studio's embedding v2 answers:
+ *
+ * ```
+ * {"status":{"code":"20000","message":"OK"},
+ * "result":{"embedding":[…1024 floats],"inputTokens":4}}
+ * ```
+ *
+ * There is no `data[]` and no `usage` object, so both are synthesized. `index` is
+ * left at 0 here — the batching loop in `embeddings.ts` rewrites it to the
+ * caller's position before the response is returned.
+ *
+ * A non-20000 status or malformed success envelope throws so an HTTP-200 error
+ * envelope can never be exposed as an empty successful embedding response.
+ */
+export function normalizeClovaEmbeddingV2Response(
+ rawData: Record
+): Record {
+ const statusCode = (rawData?.status as { code?: unknown } | undefined)?.code;
+ if (String(statusCode) !== "20000") {
+ throw new Error("CLOVA Studio embedding v2 returned an unsuccessful status");
+ }
+
+ const result = (rawData?.result ?? {}) as Record;
+ if (!Array.isArray(result.embedding)) {
+ throw new Error("CLOVA Studio embedding v2 response is missing an embedding vector");
+ }
+
+ const inputTokens = Number(result.inputTokens) || 0;
+ return {
+ data: [{ object: "embedding", index: 0, embedding: result.embedding }],
+ usage: { prompt_tokens: inputTokens, total_tokens: inputTokens },
+ };
+}
diff --git a/open-sse/handlers/embeddings.ts b/open-sse/handlers/embeddings.ts
index 7cf322610d..c60842b09f 100644
--- a/open-sse/handlers/embeddings.ts
+++ b/open-sse/handlers/embeddings.ts
@@ -1,16 +1,8 @@
/**
* Embedding Handler
*
- * Handles POST /v1/embeddings requests.
- * Proxies to upstream embedding providers using OpenAI-compatible format.
- *
- * Request format (OpenAI-compatible):
- * {
- * "model": "nebius/Qwen/Qwen3-Embedding-8B",
- * "input": "text" | ["text1", "text2"],
- * "dimensions": 4096, // optional
- * "encoding_format": "float" // optional
- * }
+ * Handles POST /v1/embeddings requests and normalizes provider responses to the
+ * OpenAI embedding shape.
*/
import {
@@ -32,6 +24,7 @@ import { stripTrailingSlashes } from "../utils/urlSanitize.ts";
import { fetchRemoteImage } from "@/shared/network/remoteImageFetch";
import {
hasStructuredEmbeddingInput,
+ normalizeClovaEmbeddingV2Response,
prepareJinaMixedEmbeddingInput,
prepareStructuredEmbeddingRequest,
} from "./embeddingStructuredInput.ts";
@@ -53,17 +46,82 @@ interface ClientRawRequest {
headers: Record;
}
-/**
- * Flatten a single embedding item's vector to the OpenAI-spec `number[]` shape.
- *
- * Some OpenAI-compatible embedding backends — notably a llama.cpp
- * `llama-server --embedding --pooling ...` instance — return each vector wrapped in one
- * extra array level: `[[...floats]]` instead of `[...floats]` for a single input. That
- * extra level is silently spec-breaking, since a standard OpenAI-SDK consumer reading
- * `response.data[i].embedding` gets a length-1 array holding the real vector instead of
- * the vector itself. Unwrap only that single redundant level; vectors that are already
- * flat (or genuinely multi-row) are left untouched. See issue #9089.
- */
+interface EmbeddingCredentials {
+ apiKey?: string | null;
+ accessToken?: string | null;
+ providerSpecificData?: Record | null;
+}
+
+interface EmbeddingLog {
+ info: (...args: unknown[]) => void;
+ error: (...args: unknown[]) => void;
+}
+
+interface HandleEmbeddingParams {
+ body: Record;
+ credentials: EmbeddingCredentials | null;
+ log?: EmbeddingLog;
+ resolvedProvider?: EmbeddingProvider | null;
+ resolvedModel?: string | null;
+ clientRawRequest?: ClientRawRequest | null;
+ apiKeyId?: string | null;
+ apiKeyName?: string | null;
+ connectionId?: string | null;
+}
+
+interface EmbeddingFailure {
+ success: false;
+ status: number;
+ error: string;
+ headers?: Headers;
+ data?: never;
+}
+
+interface EmbeddingSuccess {
+ success: true;
+ data: Record;
+ headers: Headers;
+ status?: never;
+ error?: never;
+}
+
+type EmbeddingResult = EmbeddingSuccess | EmbeddingFailure;
+
+interface ResolvedEmbedding {
+ provider: string | null;
+ model: string | null;
+ providerConfig: EmbeddingProvider | null;
+}
+
+type RequestLogger = Awaited>;
+type ProviderResponseNormalizer =
+ ((data: Record) => Record) | null;
+
+interface EmbeddingRuntime extends HandleEmbeddingParams {
+ provider: string;
+ model: string | null;
+ providerConfig: EmbeddingProvider;
+ startTime: number;
+ detailedLoggingEnabled: boolean;
+ reqLogger: RequestLogger;
+ logRequestBody: Record;
+}
+
+interface PreparedEmbeddingRequest {
+ upstreamBody: Record;
+ upstreamUrl: string;
+ headers: Record;
+ normalizeProviderResponse: ProviderResponseNormalizer;
+}
+
+interface ParsedEmbeddingResponse {
+ data?: unknown[] | unknown;
+ usage?: { prompt_tokens?: number; total_tokens?: number };
+}
+
+const KNOWN_EMBEDDING_FIELDS = new Set(["model", "input", "dimensions", "encoding_format"]);
+
+/** Unwrap one redundant row around an otherwise flat vector. */
function flattenSingleRowEmbedding(item: unknown): void {
if (!item || typeof item !== "object" || !("embedding" in item)) return;
const record = item as { embedding: unknown };
@@ -78,103 +136,81 @@ function flattenSingleRowEmbedding(item: unknown): void {
}
}
-/**
- * Handle embedding request.
- * Supports both hardcoded cloud providers and dynamic local provider_nodes.
- * When resolvedProvider is passed, uses it directly (injection pattern from route handler).
- * Falls back to hardcoded registry lookup for backward compatibility.
- */
-export async function handleEmbedding({
- body,
- credentials,
- log,
- resolvedProvider = null,
- resolvedModel = null,
- clientRawRequest = null,
- apiKeyId = null,
- apiKeyName = null,
- connectionId = null,
-}: {
- body: Record;
- credentials: {
- apiKey?: string | null;
- accessToken?: string | null;
- providerSpecificData?: Record | null;
- } | null;
- log?: { info: (...args: unknown[]) => void; error: (...args: unknown[]) => void };
- resolvedProvider?: EmbeddingProvider | null;
- resolvedModel?: string | null;
- clientRawRequest?: ClientRawRequest | null;
- apiKeyId?: string | null;
- apiKeyName?: string | null;
- connectionId?: string | null;
-}) {
- // Use pre-resolved provider/model from route handler if available (supports dynamic provider_nodes).
- let provider: string | null;
- let model: string | null;
- let providerConfig: EmbeddingProvider | null;
+function failure(status: number, error: string, headers?: Headers): EmbeddingFailure {
+ return { success: false, status, error, ...(headers ? { headers } : {}) };
+}
- if (resolvedProvider) {
- provider = resolvedProvider.id;
- model = resolvedModel;
- providerConfig = resolvedProvider;
- } else {
- const parsed = parseEmbeddingModel(body.model as string);
- provider = parsed.provider;
- model = parsed.model;
- providerConfig = provider ? getEmbeddingProvider(provider) : null;
+function resolveEmbedding(params: HandleEmbeddingParams): ResolvedEmbedding {
+ if (params.resolvedProvider) {
+ return {
+ provider: params.resolvedProvider.id,
+ model: params.resolvedModel ?? null,
+ providerConfig: params.resolvedProvider,
+ };
}
+ const parsed = parseEmbeddingModel(params.body.model as string);
+ return {
+ provider: parsed.provider,
+ model: parsed.model,
+ providerConfig: parsed.provider ? getEmbeddingProvider(parsed.provider) : null,
+ };
+}
- const startTime = Date.now();
-
- // Set up request logger for pipeline artifact capture
+async function createEmbeddingRuntime(
+ params: HandleEmbeddingParams,
+ resolved: ResolvedEmbedding
+): Promise {
const detailedLoggingEnabled = await isDetailedLoggingEnabled();
- const captureStreamChunks = getCallLogPipelineCaptureStreamChunks();
const reqLogger = await createRequestLogger(
- provider || "openai",
+ resolved.provider || "openai",
"openai",
- body.model as string,
+ params.body.model as string,
{
enabled: detailedLoggingEnabled,
- captureStreamChunks,
- connectionId: connectionId || undefined,
- model: model || (body.model as string),
- provider: provider || undefined,
+ captureStreamChunks: getCallLogPipelineCaptureStreamChunks(),
+ connectionId: params.connectionId || undefined,
+ model: resolved.model || (params.body.model as string),
+ provider: resolved.provider || undefined,
}
);
- // Log client raw request
- if (clientRawRequest) {
+ if (params.clientRawRequest) {
reqLogger.logClientRawRequest(
- clientRawRequest.endpoint,
- clientRawRequest.body,
- clientRawRequest.headers
+ params.clientRawRequest.endpoint,
+ params.clientRawRequest.body,
+ params.clientRawRequest.headers
);
}
+ if (!resolved.provider) {
+ return failure(
+ 400,
+ `Invalid embedding model: ${params.body.model}. Use format: provider/model`
+ );
+ }
+ if (!resolved.providerConfig) {
+ return failure(400, `Unknown embedding provider: ${resolved.provider}`);
+ }
- // Summarized request body for call log (avoid storing large embedding input arrays)
- const logRequestBody = {
- model: body.model,
- input_count: Array.isArray(body.input) ? body.input.length : 1,
- dimensions: body.dimensions || undefined,
+ return {
+ ...params,
+ provider: resolved.provider,
+ model: resolved.model,
+ providerConfig: resolved.providerConfig,
+ startTime: Date.now(),
+ detailedLoggingEnabled,
+ reqLogger,
+ logRequestBody: {
+ model: params.body.model,
+ input_count: Array.isArray(params.body.input) ? params.body.input.length : 1,
+ dimensions: params.body.dimensions || undefined,
+ },
};
+}
- if (!provider) {
- return {
- success: false,
- status: 400,
- error: `Invalid embedding model: ${body.model}. Use format: provider/model`,
- };
- }
-
- if (!providerConfig) {
- return {
- success: false,
- status: 400,
- error: `Unknown embedding provider: ${provider}`,
- };
- }
-
+function collectRequestedModalities(body: Record): {
+ structuredItems: Array<{ type: EmbeddingModality }>;
+ nativeModalities: EmbeddingModality[];
+} {
const structuredItems = Array.isArray(body.input)
? body.input.filter(
(item): item is { type: EmbeddingModality } =>
@@ -184,409 +220,493 @@ export async function handleEmbedding({
const nativeModalities = [
...(isJinaNativeEmbeddingInput(body.input) ? collectJinaNativeModalities(body.input) : []),
...(isGeminiNativeEmbeddingInput(body.input) ? collectGeminiNativeModalities(body.input) : []),
- ].filter((modality) => modality !== "text");
- if (structuredItems.length > 0 || nativeModalities.length > 0) {
- const supportedModalities = getEmbeddingModelModalities(providerConfig, model);
- if (!supportedModalities) {
- return {
- success: false,
- status: 400,
- error: `Embedding model ${body.model} does not advertise structured embedding input support`,
- };
- }
- const unsupportedCanonical = structuredItems.find(
- (item) => !supportedModalities.includes(item.type)
+ ].filter((modality): modality is EmbeddingModality => modality !== "text");
+ return { structuredItems, nativeModalities };
+}
+
+function validateRequestedModalities(runtime: EmbeddingRuntime): EmbeddingFailure | null {
+ const { structuredItems, nativeModalities } = collectRequestedModalities(runtime.body);
+ if (structuredItems.length === 0 && nativeModalities.length === 0) return null;
+
+ const supported = getEmbeddingModelModalities(runtime.providerConfig, runtime.model);
+ if (!supported) {
+ return failure(
+ 400,
+ `Embedding model ${runtime.body.model} does not advertise structured embedding input support`
);
- if (unsupportedCanonical) {
- return {
- success: false,
- status: 400,
- error: `Embedding model ${body.model} does not support ${unsupportedCanonical.type} input`,
- };
- }
- const unsupportedNative = nativeModalities.find(
- (modality) => !supportedModalities.includes(modality)
- );
- if (unsupportedNative) {
- return {
- success: false,
- status: 400,
- error: `Embedding model ${body.model} does not support ${unsupportedNative} input`,
- };
- }
}
+ const unsupportedCanonical = structuredItems.find((item) => !supported.includes(item.type));
+ if (unsupportedCanonical) {
+ return failure(
+ 400,
+ `Embedding model ${runtime.body.model} does not support ${unsupportedCanonical.type} input`
+ );
+ }
+ const unsupportedNative = nativeModalities.find((modality) => !supported.includes(modality));
+ return unsupportedNative
+ ? failure(
+ 400,
+ `Embedding model ${runtime.body.model} does not support ${unsupportedNative} input`
+ )
+ : null;
+}
- // Build upstream request — start with standard fields, then forward extra fields
- // the client sent (e.g. input_type, user, truncate for NVIDIA NIM asymmetric models).
- const KNOWN_FIELDS = new Set(["model", "input", "dimensions", "encoding_format"]);
-
- let upstreamBody: Record = {
- model: model,
- input: body.input,
+function buildUpstreamBody(runtime: EmbeddingRuntime): Record {
+ const upstreamBody: Record = {
+ model: runtime.model,
+ input: runtime.body.input,
};
-
- if (body.dimensions !== undefined) upstreamBody.dimensions = body.dimensions;
- if (body.encoding_format !== undefined) upstreamBody.encoding_format = body.encoding_format;
-
- for (const [key, value] of Object.entries(body)) {
- if (!KNOWN_FIELDS.has(key) && value !== undefined) {
- upstreamBody[key] = value;
- }
+ if (runtime.body.dimensions !== undefined) upstreamBody.dimensions = runtime.body.dimensions;
+ if (runtime.body.encoding_format !== undefined) {
+ upstreamBody.encoding_format = runtime.body.encoding_format;
+ }
+ for (const [key, value] of Object.entries(runtime.body)) {
+ if (!KNOWN_EMBEDDING_FIELDS.has(key) && value !== undefined) upstreamBody[key] = value;
}
- // Gemini embedding models (gemini-embedding-001 / -2-preview / text-embedding-004)
- // default to 3072-dim vectors. Clients targeting pgvector-style schemas typically
- // request a smaller size (e.g. 1536) via OpenAI's `dimensions` field, but Google's
- // OpenAI-compatibility shim at /v1beta/openai/embeddings does not document the
- // `dimensions` → `outputDimensionality` translation. Mirror the request value into
- // the Gemini-native `outputDimensionality` field so the upstream actually returns
- // the requested vector size. Ported from upstream decolua/9router#1366.
- if (provider === "gemini" && upstreamBody.outputDimensionality === undefined) {
- const outputDimensionality = Number(body.dimensions);
+ if (runtime.provider === "gemini" && upstreamBody.outputDimensionality === undefined) {
+ const outputDimensionality = Number(runtime.body.dimensions);
if (Number.isFinite(outputDimensionality) && outputDimensionality > 0) {
upstreamBody.outputDimensionality = outputDimensionality;
}
}
-
- // Inject model-level default params (e.g. NVIDIA NIM asymmetric models require
- // `input_type`) only for keys the client did not already supply, so a
- // client-sent value is never overwritten. Symmetric models carry no defaults
- // and are unaffected. See issue #1378.
- const defaultParams = getEmbeddingModelDefaultParams(providerConfig, model);
- if (defaultParams) {
- for (const [key, value] of Object.entries(defaultParams)) {
- if (upstreamBody[key] === undefined) {
- upstreamBody[key] = value;
- }
- }
+ const defaultParams = getEmbeddingModelDefaultParams(runtime.providerConfig, runtime.model);
+ for (const [key, value] of Object.entries(defaultParams ?? {})) {
+ if (upstreamBody[key] === undefined) upstreamBody[key] = value;
}
+ return upstreamBody;
+}
- let upstreamUrl = providerConfig.baseUrl;
- if (provider === "ollama-local" || provider === "lmstudio") {
- // Keyless local servers (#2824 ollama-local, #11233 lmstudio): honor the
- // configured connection's baseUrl when one was hydrated, and fall back to
- // the static localhost registry default otherwise.
- const configuredBaseUrl = credentials?.providerSpecificData?.baseUrl;
- const rawBaseUrl =
- typeof configuredBaseUrl === "string" && configuredBaseUrl.trim().length > 0
- ? configuredBaseUrl
- : providerConfig.baseUrl;
- // Use the shared O(n) helper instead of `/\/+$/` — that regex is
- // vulnerable to polynomial backtracking on adversarial input
- // (CodeQL js/polynomial-redos) since baseUrl is operator-configured
- // per-connection data. See open-sse/utils/urlSanitize.ts.
- const normalizedBaseUrl = stripTrailingSlashes(rawBaseUrl.trim());
- const localServerHost = normalizedBaseUrl
- .replace(/\/v1\/(?:chat\/completions|embeddings)$/i, "")
- .replace(/\/api\/chat$/i, "")
- .replace(/\/v1$/i, "");
- upstreamUrl = `${localServerHost}/v1/embeddings`;
- }
- let normalizeProviderResponse:
- ((data: Record) => Record) | null = null;
+function resolveLocalEmbeddingUrl(runtime: EmbeddingRuntime): string {
+ const configuredBaseUrl = runtime.credentials?.providerSpecificData?.baseUrl;
+ const rawBaseUrl =
+ typeof configuredBaseUrl === "string" && configuredBaseUrl.trim()
+ ? configuredBaseUrl
+ : runtime.providerConfig.baseUrl;
+ const localServerHost = stripTrailingSlashes(rawBaseUrl.trim())
+ .replace(/\/v1\/(?:chat\/completions|embeddings)$/i, "")
+ .replace(/\/api\/chat$/i, "")
+ .replace(/\/v1$/i, "");
+ return `${localServerHost}/v1/embeddings`;
+}
- // Build headers
- const headers: Record = {
- "Content-Type": "application/json",
- };
+function resolveUpstreamUrl(runtime: EmbeddingRuntime): string {
+ return runtime.provider === "ollama-local" || runtime.provider === "lmstudio"
+ ? resolveLocalEmbeddingUrl(runtime)
+ : runtime.providerConfig.baseUrl;
+}
- // Skip credential injection for local providers (authType: "none")
+function buildAuth(
+ runtime: EmbeddingRuntime
+): { headers: Record; token: string | null } | EmbeddingFailure {
+ const headers: Record = { "Content-Type": "application/json" };
const token =
- providerConfig.authType === "none" ? null : credentials?.apiKey || credentials?.accessToken;
- if (token) {
- if (providerConfig.authHeader === "bearer") {
- headers["Authorization"] = `Bearer ${token}`;
- } else if (providerConfig.authHeader === "x-api-key") {
- headers["x-api-key"] = token;
- }
- } else if (providerConfig.authType !== "none") {
- return {
- success: false,
- status: 401,
- error: `No valid authentication token for provider ${provider}. Check provider credentials.`,
- };
- }
-
- // Jina v5 Omni native docs ({ text }, { image: url|base64 }, { content: [...] })
- // must reach api.jina.ai unchanged. Do not fetch those image URLs or collapse
- // to string[]. Canonical { type, source } items still go through the translator.
- const jinaNative = isJinaNativeEmbeddingInput(body.input);
- const geminiNative = isGeminiNativeEmbeddingInput(body.input);
- const canonicalStructured = hasStructuredEmbeddingInput(body.input);
- const passThroughJinaNative =
- providerConfig.structuredInputProtocol === "jina-v1" && jinaNative && !canonicalStructured;
- // gemini-embedding-2 aggregates a string[] on Google's OpenAI shim into one
- // vector. Always use embedContent / batchEmbedContents so N input items
- // become N embeddings. Native multimodal parts take the same path.
- const useGeminiNativeTransport =
- providerConfig.structuredInputProtocol === "gemini-embed-content" &&
- (isGeminiEmbedding2Family(model) || canonicalStructured || geminiNative || jinaNative);
-
- if (providerConfig.structuredInputProtocol === "jina-v1" && jinaNative && canonicalStructured) {
- try {
- const mixed = Array.isArray(body.input) ? body.input : [body.input];
- upstreamBody.input = await prepareJinaMixedEmbeddingInput(mixed, async (url) => {
- const result = await fetchRemoteImage(url, {
- guard: "public-only",
- maxBytes: MAX_EMBEDDING_INLINE_ITEM_BYTES,
- pinDns: true,
- });
- return { buffer: result.buffer, contentType: result.contentType || null };
- });
- } catch (error) {
- return { success: false, status: 400, error: sanitizeErrorMessage(error) };
- }
- } else if (useGeminiNativeTransport || (!passThroughJinaNative && canonicalStructured)) {
- if (!model) {
- return {
- success: false,
- status: 400,
- error: `Invalid embedding model: ${body.model}. Use format: provider/model`,
- };
- }
- try {
- const prepared = await prepareStructuredEmbeddingRequest(
- providerConfig,
- model,
- body,
- token ?? "",
- {
- fetchMedia: async (url) => {
- const result = await fetchRemoteImage(url, {
- guard: "public-only",
- maxBytes: MAX_EMBEDDING_INLINE_ITEM_BYTES,
- pinDns: true,
- });
- return { buffer: result.buffer, contentType: result.contentType || null };
- },
- }
- );
- upstreamBody = prepared.body;
- upstreamUrl = prepared.url;
- normalizeProviderResponse = prepared.normalizeResponse ?? null;
- if (prepared.authHeader) {
- delete headers.Authorization;
- delete headers["x-api-key"];
- headers[prepared.authHeader.name] = prepared.authHeader.value;
- }
- } catch (error) {
- return { success: false, status: 400, error: sanitizeErrorMessage(error) };
- }
- }
-
- if (log) {
- log.info(
- "EMBED",
- `${provider}/${model} | input: ${Array.isArray(body.input) ? body.input.length + " items" : "1 item"}`
+ runtime.providerConfig.authType === "none"
+ ? null
+ : runtime.credentials?.apiKey || runtime.credentials?.accessToken || null;
+ if (!token && runtime.providerConfig.authType !== "none") {
+ return failure(
+ 401,
+ `No valid authentication token for provider ${runtime.provider}. Check provider credentials.`
);
}
+ if (token && runtime.providerConfig.authHeader === "bearer") {
+ headers.Authorization = `Bearer ${token}`;
+ } else if (token && runtime.providerConfig.authHeader === "x-api-key") {
+ headers["x-api-key"] = token;
+ }
+ return { headers, token };
+}
- try {
- // Quota share enforcement (fail-open: errors allow the request through)
- if (apiKeyId && connectionId && provider) {
- try {
- const { enforceQuotaShare } = await import("@/lib/quota/enforce");
- const quotaDecision = await enforceQuotaShare({
- apiKeyId,
- connectionId,
- provider,
- // Per-(key,model) cap — resolved embedding model id (same scope used in logs/routing).
- model: model || undefined,
- });
- if (quotaDecision.kind === "block") {
- return {
- success: false,
- status: quotaDecision.httpStatus ?? 429,
- error: quotaDecision.reason || "Quota share limit reached",
- };
- }
- } catch {
- // fail-open per B16
- }
- }
+async function fetchEmbeddingMedia(
+ url: string
+): Promise<{ buffer: Buffer; contentType: string | null }> {
+ const result = await fetchRemoteImage(url, {
+ guard: "public-only",
+ maxBytes: MAX_EMBEDDING_INLINE_ITEM_BYTES,
+ pinDns: true,
+ });
+ return { buffer: result.buffer, contentType: result.contentType || null };
+}
- // Log provider request
- reqLogger.logTargetRequest(upstreamUrl, headers, upstreamBody);
+async function prepareMixedJinaInput(
+ runtime: EmbeddingRuntime,
+ prepared: PreparedEmbeddingRequest
+): Promise {
+ const mixed = Array.isArray(runtime.body.input) ? runtime.body.input : [runtime.body.input];
+ prepared.upstreamBody.input = await prepareJinaMixedEmbeddingInput(mixed, fetchEmbeddingMedia);
+}
- const response = await fetch(upstreamUrl, {
- method: "POST",
- headers,
- body: JSON.stringify(upstreamBody),
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- if (log) {
- log.error("EMBED", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
- }
-
- // Log provider response
- reqLogger.logProviderResponse(response.status, "", response.headers, errorText.slice(0, 500));
-
- // Build client error response
- const clientErrorBody = toJsonErrorPayload(
- errorText.slice(0, 500),
- "Embedding provider error"
- );
- reqLogger.logConvertedResponse(clientErrorBody);
-
- const pipelinePayloads = detailedLoggingEnabled ? reqLogger.getPipelinePayloads() : null;
-
- // Save error call log for Logger panel
- saveCallLog({
- method: "POST",
- path: "/v1/embeddings",
- status: response.status,
- model: `${provider}/${model}`,
- provider,
- duration: Date.now() - startTime,
- error: errorText.slice(0, 500),
- requestBody: logRequestBody,
- pipelinePayloads,
- apiKeyId,
- apiKeyName,
- 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,
- error: errorText,
- headers: stripStaleEncodingHeaders(response.headers),
- };
- }
-
- const rawData = (await response.json()) as Record;
- const data = (normalizeProviderResponse ? normalizeProviderResponse(rawData) : rawData) as {
- data?: unknown[] | unknown;
- usage?: { prompt_tokens?: number; total_tokens?: number };
- };
-
- // Log provider response
- reqLogger.logProviderResponse(response.status, "", response.headers, data);
-
- // OpenAI-spec compliance (#9089): each item's `embedding` must be a flat number[].
- // Some OpenAI-compatible backends (e.g. a llama.cpp `llama-server --embedding`
- // instance) return the vector wrapped in one extra array level — `[[...floats]]`
- // instead of `[...floats]` — for a single input, which silently breaks any standard
- // OpenAI-SDK consumer doing `response.data[i].embedding`. Flatten that one redundant
- // level without touching providers that already return flat vectors.
- const responseItems = data.data || data;
- if (Array.isArray(responseItems)) {
- for (const item of responseItems) {
- flattenSingleRowEmbedding(item);
- }
- }
-
- // Normalize response to OpenAI format
- const normalizedResponse = {
- object: "list",
- data: data.data || data,
- model: `${provider}/${model}`,
- usage: data.usage || { prompt_tokens: 0, total_tokens: 0 },
- };
-
- // Log client response
- reqLogger.logConvertedResponse(normalizedResponse);
-
- const pipelinePayloads = detailedLoggingEnabled ? reqLogger.getPipelinePayloads() : null;
-
- // Save success call log for Logger panel
- // Embeddings only have input tokens (prompt_tokens + total_tokens), no output/completion tokens
- saveCallLog({
- method: "POST",
- path: "/v1/embeddings",
- status: 200,
- model: `${provider}/${model}`,
- provider,
- duration: Date.now() - startTime,
- tokens: {
- prompt_tokens: data.usage?.prompt_tokens || data.usage?.total_tokens || 0,
- completion_tokens: 0,
- },
- requestBody: logRequestBody,
- responseBody: {
- usage: data.usage || null,
- object: "list",
- data_count: Array.isArray(data.data) ? data.data.length : 0,
- },
- pipelinePayloads,
- apiKeyId,
- apiKeyName,
- connectionId,
- }).catch(() => {});
-
- // Record quota consumption (fire-and-forget, never blocks)
- if (apiKeyId && connectionId && provider) {
- try {
- const { scheduleRecordConsumption } = await import("@/lib/quota/spendRecorder");
- scheduleRecordConsumption({
- apiKeyId,
- connectionId,
- provider,
- // Per-(key,model) cap accounting — same resolved model id used at enforce time.
- model: model || undefined,
- cost: {
- tokens: data.usage?.prompt_tokens || data.usage?.total_tokens || 0,
- requests: 1,
- },
- });
- } catch {
- // fail-open per B29
- }
- }
-
- return {
- success: true,
- data: normalizedResponse,
- headers: stripStaleEncodingHeaders(response.headers),
- };
- } catch (err) {
- if (log) {
- log.error("EMBED", `${provider} fetch error: ${err.message}`);
- }
-
- // Log error
- reqLogger.logError(err, upstreamBody);
-
- const pipelinePayloads = detailedLoggingEnabled ? reqLogger.getPipelinePayloads() : null;
-
- // Save exception call log for Logger panel
- saveCallLog({
- method: "POST",
- path: "/v1/embeddings",
- status: 502,
- model: `${provider}/${model}`,
- provider,
- duration: Date.now() - startTime,
- error: err.message,
- requestBody: logRequestBody,
- pipelinePayloads,
- apiKeyId,
- apiKeyName,
- connectionId,
- }).catch(() => {});
-
- return {
- success: false,
- status: 502,
- error: `Embedding provider error: ${sanitizeErrorMessage(err.message)}`,
- };
+async function prepareNativeTransport(
+ runtime: EmbeddingRuntime,
+ prepared: PreparedEmbeddingRequest,
+ token: string | null
+): Promise {
+ if (!runtime.model) {
+ throw new Error(`Invalid embedding model: ${runtime.body.model}. Use format: provider/model`);
+ }
+ const native = await prepareStructuredEmbeddingRequest(
+ runtime.providerConfig,
+ runtime.model,
+ runtime.body,
+ token ?? "",
+ { fetchMedia: fetchEmbeddingMedia }
+ );
+ prepared.upstreamBody = native.body;
+ prepared.upstreamUrl = native.url;
+ prepared.normalizeProviderResponse = native.normalizeResponse ?? null;
+ if (native.authHeader) {
+ delete prepared.headers.Authorization;
+ delete prepared.headers["x-api-key"];
+ prepared.headers[native.authHeader.name] = native.authHeader.value;
}
}
+
+async function applyStructuredTransport(
+ runtime: EmbeddingRuntime,
+ prepared: PreparedEmbeddingRequest,
+ token: string | null
+): Promise {
+ const jinaNative = isJinaNativeEmbeddingInput(runtime.body.input);
+ const geminiNative = isGeminiNativeEmbeddingInput(runtime.body.input);
+ const canonical = hasStructuredEmbeddingInput(runtime.body.input);
+ const isJinaProtocol = runtime.providerConfig.structuredInputProtocol === "jina-v1";
+ const passThroughJina = isJinaProtocol && jinaNative && !canonical;
+ const useGeminiNative =
+ runtime.providerConfig.structuredInputProtocol === "gemini-embed-content" &&
+ (isGeminiEmbedding2Family(runtime.model) || canonical || geminiNative || jinaNative);
+
+ if (isJinaProtocol && jinaNative && canonical) {
+ await prepareMixedJinaInput(runtime, prepared);
+ } else if (useGeminiNative || (!passThroughJina && canonical)) {
+ await prepareNativeTransport(runtime, prepared, token);
+ }
+}
+
+async function prepareEmbeddingRequest(
+ runtime: EmbeddingRuntime
+): Promise {
+ const auth = buildAuth(runtime);
+ if ("success" in auth) return auth;
+ const prepared: PreparedEmbeddingRequest = {
+ upstreamBody: buildUpstreamBody(runtime),
+ upstreamUrl: resolveUpstreamUrl(runtime),
+ headers: auth.headers,
+ normalizeProviderResponse: null,
+ };
+ try {
+ await applyStructuredTransport(runtime, prepared, auth.token);
+ return prepared;
+ } catch (error) {
+ return failure(400, sanitizeErrorMessage(error));
+ }
+}
+
+async function enforceEmbeddingQuota(runtime: EmbeddingRuntime): Promise {
+ if (!runtime.apiKeyId || !runtime.connectionId) return null;
+ try {
+ const { enforceQuotaShare } = await import("@/lib/quota/enforce");
+ const decision = await enforceQuotaShare({
+ apiKeyId: runtime.apiKeyId,
+ connectionId: runtime.connectionId,
+ provider: runtime.provider,
+ model: runtime.model || undefined,
+ });
+ return decision.kind === "block"
+ ? failure(decision.httpStatus ?? 429, decision.reason || "Quota share limit reached")
+ : null;
+ } catch {
+ return null;
+ }
+}
+
+function resolveSingleTexts(runtime: EmbeddingRuntime): string[] | EmbeddingFailure | null {
+ if (runtime.providerConfig.singleTextProtocol !== "clova-v2") return null;
+ const input = Array.isArray(runtime.body.input) ? runtime.body.input : [runtime.body.input];
+ if (
+ input.length === 0 ||
+ input.some((item) => typeof item !== "string" || item.trim().length === 0)
+ ) {
+ return failure(400, "CLOVA Studio embedding v2 accepts non-empty text strings only");
+ }
+ if (runtime.body.encoding_format === "base64") {
+ return failure(400, "CLOVA Studio embedding v2 supports float encoding only");
+ }
+ if (runtime.body.dimensions !== undefined && Number(runtime.body.dimensions) !== 1024) {
+ return failure(400, "CLOVA Studio embedding v2 has a fixed dimension of 1024");
+ }
+ return input as string[];
+}
+
+function appendClovaEmbedding(
+ parsed: ParsedEmbeddingResponse,
+ embeddings: Array>,
+ usage: { prompt_tokens: number; total_tokens: number }
+): void {
+ if (!Array.isArray(parsed.data)) {
+ throw new Error("CLOVA Studio embedding v2 returned an invalid data list");
+ }
+ for (const item of parsed.data) {
+ flattenSingleRowEmbedding(item);
+ if (!item || typeof item !== "object") {
+ throw new Error("CLOVA Studio embedding v2 returned an invalid embedding item");
+ }
+ (item as { index?: number }).index = embeddings.length;
+ embeddings.push(item as Record);
+ }
+ usage.prompt_tokens += parsed.usage?.prompt_tokens || parsed.usage?.total_tokens || 0;
+ usage.total_tokens += parsed.usage?.total_tokens || parsed.usage?.prompt_tokens || 0;
+}
+
+async function fetchClovaEmbeddingBatch(
+ prepared: PreparedEmbeddingRequest,
+ texts: string[],
+ reqLogger: RequestLogger
+): Promise {
+ const embeddings: Array> = [];
+ const usage = { prompt_tokens: 0, total_tokens: 0 };
+ let lastHeaders = new Headers();
+ for (const text of texts) {
+ const requestBody = { text };
+ reqLogger.logTargetRequest(prepared.upstreamUrl, prepared.headers, requestBody);
+ const response = await fetch(prepared.upstreamUrl, {
+ method: "POST",
+ headers: prepared.headers,
+ body: JSON.stringify(requestBody),
+ });
+ lastHeaders = response.headers;
+ if (!response.ok) return response;
+ const rawData = (await response.json()) as Record;
+ appendClovaEmbedding(normalizeClovaEmbeddingV2Response(rawData), embeddings, usage);
+ }
+ return new Response(JSON.stringify({ data: embeddings, usage }), {
+ status: 200,
+ headers: lastHeaders,
+ });
+}
+
+async function dispatchEmbeddingRequest(
+ prepared: PreparedEmbeddingRequest,
+ singleTexts: string[] | null,
+ reqLogger: RequestLogger
+): Promise {
+ if (singleTexts) return fetchClovaEmbeddingBatch(prepared, singleTexts, reqLogger);
+ reqLogger.logTargetRequest(prepared.upstreamUrl, prepared.headers, prepared.upstreamBody);
+ return fetch(prepared.upstreamUrl, {
+ method: "POST",
+ headers: prepared.headers,
+ body: JSON.stringify(prepared.upstreamBody),
+ });
+}
+
+function pipelinePayloads(
+ runtime: EmbeddingRuntime
+): ReturnType | null {
+ return runtime.detailedLoggingEnabled ? runtime.reqLogger.getPipelinePayloads() : null;
+}
+
+async function handleUpstreamFailure(
+ runtime: EmbeddingRuntime,
+ response: Response
+): Promise {
+ const errorText = await response.text();
+ runtime.log?.error(
+ "EMBED",
+ `${runtime.provider} error ${response.status}: ${errorText.slice(0, 200)}`
+ );
+ runtime.reqLogger.logProviderResponse(
+ response.status,
+ "",
+ response.headers,
+ errorText.slice(0, 500)
+ );
+ runtime.reqLogger.logConvertedResponse(
+ toJsonErrorPayload(errorText.slice(0, 500), "Embedding provider error")
+ );
+ saveCallLog({
+ method: "POST",
+ path: "/v1/embeddings",
+ status: response.status,
+ model: `${runtime.provider}/${runtime.model}`,
+ provider: runtime.provider,
+ duration: Date.now() - runtime.startTime,
+ error: errorText.slice(0, 500),
+ requestBody: runtime.logRequestBody,
+ pipelinePayloads: pipelinePayloads(runtime),
+ apiKeyId: runtime.apiKeyId,
+ apiKeyName: runtime.apiKeyName,
+ connectionId: runtime.connectionId,
+ }).catch(() => {});
+ if (runtime.connectionId) {
+ try {
+ await markAccountUnavailable(
+ runtime.connectionId,
+ response.status,
+ errorText,
+ runtime.provider,
+ runtime.model
+ );
+ } catch {
+ // The upstream response has priority over a best-effort cooldown write.
+ }
+ }
+ return failure(response.status, errorText, stripStaleEncodingHeaders(response.headers));
+}
+
+function normalizeEmbeddingData(
+ runtime: EmbeddingRuntime,
+ response: Response,
+ rawData: Record,
+ normalizer: ProviderResponseNormalizer
+): { data: ParsedEmbeddingResponse; normalizedResponse: Record } {
+ const data = (normalizer ? normalizer(rawData) : rawData) as ParsedEmbeddingResponse;
+ runtime.reqLogger.logProviderResponse(response.status, "", response.headers, data);
+ const responseItems = data.data || data;
+ if (Array.isArray(responseItems)) responseItems.forEach(flattenSingleRowEmbedding);
+ return {
+ data,
+ normalizedResponse: {
+ object: "list",
+ data: data.data || data,
+ model: `${runtime.provider}/${runtime.model}`,
+ usage: data.usage || { prompt_tokens: 0, total_tokens: 0 },
+ },
+ };
+}
+
+function recordEmbeddingSuccess(
+ runtime: EmbeddingRuntime,
+ data: ParsedEmbeddingResponse,
+ normalizedResponse: Record
+): void {
+ runtime.reqLogger.logConvertedResponse(normalizedResponse);
+ saveCallLog({
+ method: "POST",
+ path: "/v1/embeddings",
+ status: 200,
+ model: `${runtime.provider}/${runtime.model}`,
+ provider: runtime.provider,
+ duration: Date.now() - runtime.startTime,
+ tokens: {
+ prompt_tokens: data.usage?.prompt_tokens || data.usage?.total_tokens || 0,
+ completion_tokens: 0,
+ },
+ requestBody: runtime.logRequestBody,
+ responseBody: {
+ usage: data.usage || null,
+ object: "list",
+ data_count: Array.isArray(data.data) ? data.data.length : 0,
+ },
+ pipelinePayloads: pipelinePayloads(runtime),
+ apiKeyId: runtime.apiKeyId,
+ apiKeyName: runtime.apiKeyName,
+ connectionId: runtime.connectionId,
+ }).catch(() => {});
+}
+
+async function recordEmbeddingConsumption(
+ runtime: EmbeddingRuntime,
+ data: ParsedEmbeddingResponse,
+ requestCount: number
+): Promise {
+ if (!runtime.apiKeyId || !runtime.connectionId) return;
+ try {
+ const { scheduleRecordConsumption } = await import("@/lib/quota/spendRecorder");
+ scheduleRecordConsumption({
+ apiKeyId: runtime.apiKeyId,
+ connectionId: runtime.connectionId,
+ provider: runtime.provider,
+ model: runtime.model || undefined,
+ cost: {
+ tokens: data.usage?.prompt_tokens || data.usage?.total_tokens || 0,
+ requests: requestCount,
+ },
+ });
+ } catch {
+ // Quota accounting is fail-open.
+ }
+}
+
+async function handleUpstreamSuccess(
+ runtime: EmbeddingRuntime,
+ prepared: PreparedEmbeddingRequest,
+ response: Response,
+ requestCount: number
+): Promise {
+ const rawData = (await response.json()) as Record;
+ const { data, normalizedResponse } = normalizeEmbeddingData(
+ runtime,
+ response,
+ rawData,
+ prepared.normalizeProviderResponse
+ );
+ recordEmbeddingSuccess(runtime, data, normalizedResponse);
+ await recordEmbeddingConsumption(runtime, data, requestCount);
+ return {
+ success: true,
+ data: normalizedResponse,
+ headers: stripStaleEncodingHeaders(response.headers),
+ };
+}
+
+function handleEmbeddingException(
+ runtime: EmbeddingRuntime,
+ prepared: PreparedEmbeddingRequest,
+ error: unknown
+): EmbeddingFailure {
+ const message = error instanceof Error ? error.message : String(error);
+ runtime.log?.error("EMBED", `${runtime.provider} fetch error: ${message}`);
+ runtime.reqLogger.logError(error, prepared.upstreamBody);
+ saveCallLog({
+ method: "POST",
+ path: "/v1/embeddings",
+ status: 502,
+ model: `${runtime.provider}/${runtime.model}`,
+ provider: runtime.provider,
+ duration: Date.now() - runtime.startTime,
+ error: message,
+ requestBody: runtime.logRequestBody,
+ pipelinePayloads: pipelinePayloads(runtime),
+ apiKeyId: runtime.apiKeyId,
+ apiKeyName: runtime.apiKeyName,
+ connectionId: runtime.connectionId,
+ }).catch(() => {});
+ return failure(502, `Embedding provider error: ${sanitizeErrorMessage(message)}`);
+}
+
+async function executeEmbedding(
+ runtime: EmbeddingRuntime,
+ prepared: PreparedEmbeddingRequest
+): Promise {
+ const quotaFailure = await enforceEmbeddingQuota(runtime);
+ if (quotaFailure) return quotaFailure;
+ const singleTextsOrFailure = resolveSingleTexts(runtime);
+ if (singleTextsOrFailure && !Array.isArray(singleTextsOrFailure)) return singleTextsOrFailure;
+ const singleTexts = Array.isArray(singleTextsOrFailure) ? singleTextsOrFailure : null;
+ try {
+ const response = await dispatchEmbeddingRequest(prepared, singleTexts, runtime.reqLogger);
+ return response.ok
+ ? handleUpstreamSuccess(runtime, prepared, response, singleTexts?.length ?? 1)
+ : handleUpstreamFailure(runtime, response);
+ } catch (error) {
+ return handleEmbeddingException(runtime, prepared, error);
+ }
+}
+
+/** Handle one OpenAI-compatible embedding request. */
+export async function handleEmbedding(params: HandleEmbeddingParams): Promise {
+ const resolved = resolveEmbedding(params);
+ const runtime = await createEmbeddingRuntime(params, resolved);
+ if ("success" in runtime) return runtime;
+ const modalityFailure = validateRequestedModalities(runtime);
+ if (modalityFailure) return modalityFailure;
+ const prepared = await prepareEmbeddingRequest(runtime);
+ if ("success" in prepared) return prepared;
+ runtime.log?.info(
+ "EMBED",
+ `${runtime.provider}/${runtime.model} | input: ${
+ Array.isArray(runtime.body.input) ? `${runtime.body.input.length} items` : "1 item"
+ }`
+ );
+ return executeEmbedding(runtime, prepared);
+}
diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts
index ce2d2af227..66d00d099b 100644
--- a/open-sse/handlers/responseSanitizer.ts
+++ b/open-sse/handlers/responseSanitizer.ts
@@ -1061,6 +1061,7 @@ function convertOpenAIResponseToResponses(openaiResponse: JsonRecord): JsonRecor
/**
* Sanitize a streaming SSE chunk for passthrough mode.
* Lighter than full sanitization — only strips problematic extra fields.
+ * Fast-path: returns original when no mutations are needed.
*/
export function sanitizeStreamingChunk(parsed: unknown): unknown {
const parsedRecord = toRecord(parsed);
@@ -1078,14 +1079,29 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
if (eventType === "content_block_delta") {
const deltaRecord = toRecord(parsedRecord.delta);
if (deltaRecord) {
+ let mutated = false;
if (typeof deltaRecord.text === "string") {
deltaRecord.text = stripZeroWidthText(deltaRecord.text);
+ mutated = true;
}
if (typeof deltaRecord.thinking === "string") {
deltaRecord.thinking = stripZeroWidthText(deltaRecord.thinking);
+ mutated = true;
}
+ return mutated ? parsedRecord : parsed;
}
- return parsedRecord;
+ return parsed;
+ }
+
+ // Fast-path: check if any mutations would actually be needed
+ // Most passthrough chunks (content deltas) need no sanitization
+ const needsIdNormalization = parsedRecord.id !== undefined && parsedRecord.id !== null && typeof parsedRecord.id !== "string";
+ const hasChoices = Array.isArray(parsedRecord.choices) && parsedRecord.choices.length > 0;
+ const hasUsage = parsedRecord.usage !== undefined;
+ const hasSystemFingerprint = parsedRecord.system_fingerprint !== undefined;
+ if (!needsIdNormalization && !hasChoices && !hasUsage && !hasSystemFingerprint) {
+ // Nothing to sanitize — forward original
+ return parsed;
}
// Build sanitized chunk
diff --git a/open-sse/mcp-server/__tests__/a2aLifecycle.test.ts b/open-sse/mcp-server/__tests__/a2aLifecycle.test.ts
index 470dc1f2bd..b660006a0f 100644
--- a/open-sse/mcp-server/__tests__/a2aLifecycle.test.ts
+++ b/open-sse/mcp-server/__tests__/a2aLifecycle.test.ts
@@ -1,11 +1,21 @@
import { afterEach, describe, expect, it } from "vitest";
-import { A2ATaskManager } from "../../../src/lib/a2a/taskManager.ts";
+import { A2ATaskManager, type A2APersistence } from "../../../src/lib/a2a/taskManager.ts";
import { executeA2ATaskWithState } from "../../../src/lib/a2a/taskExecution.ts";
const managers: A2ATaskManager[] = [];
+// Default persistence opens SQLite (167 migrations) inside the vitest thread pool.
+// Tests inject a no-op so they never touch the DB (same seam as a2a-task-persistence.test.ts).
+function noopPersistence(): A2APersistence {
+ return {
+ upsert: (() => {}) as A2APersistence["upsert"],
+ appendEvent: (() => {}) as A2APersistence["appendEvent"],
+ purge: ((): number => 0) as A2APersistence["purge"],
+ };
+}
+
function createManager(ttlMinutes = 5) {
- const manager = new A2ATaskManager(ttlMinutes);
+ const manager = new A2ATaskManager(ttlMinutes, noopPersistence());
managers.push(manager);
return manager;
}
@@ -44,9 +54,14 @@ describe("A2A task lifecycle regressions", () => {
tm.updateTask(task.id, "working");
await expect(
- executeA2ATaskWithState(tm, task, async () => {
- throw new Error("upstream failure");
- })
+ executeA2ATaskWithState(
+ tm,
+ task,
+ async () => {
+ throw new Error("upstream failure");
+ },
+ { search: async () => [], appendEvent: () => {} }
+ )
).rejects.toThrow("upstream failure");
const loaded = tm.getTask(task.id);
diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts
index 36f1c8b766..c6465321f9 100644
--- a/open-sse/services/accountFallback.ts
+++ b/open-sse/services/accountFallback.ts
@@ -49,7 +49,8 @@ import {
} from "../../src/shared/constants/providers";
import { resolveUseUpstream429BreakerHints } from "../../src/shared/utils/providerHints";
import { getCodexModelScope } from "../config/codexQuotaScopes.ts";
-import { getQuotaScopedModelForProvider } from "./antigravityQuotaFamily.ts";
+import { getQuotaScopedModelForProvider, isAntigravityQuotaProvider } from "./antigravityQuotaFamily.ts";
+import { persistAntigravityFamilyCooldownIfQuota } from "./antigravityFamilyCooldown.ts";
import {
classifyGeminiQuotaMetricFromText,
isRpdExhausted,
@@ -63,6 +64,18 @@ import {
parseDelayString,
MAX_SHORT_RETRY_HINT_MS,
} from "./retryAfterJson.ts";
+import { isMoonshotAccountBalanceExhausted } from "./usage/moonshotOpenPlatform.ts";
+import { isTpdRateLimit, resolveTpdCooldownMs } from "./dailyQuotaReset.ts";
+
+// Pre-compiled regex constants for hot-path retry parsing (avoid per-call compilation)
+const RETRY_AFTER_RE = /retry\s+after\s+(\d+)\s*s/i;
+const PLEASE_RETRY_RE = /please retry in\s+([\d.]+\s*s)/i;
+const ISO_RETRY_RE = /\b(?:try again at|wait until|reset(?:s)? at|available at|retry after)\s+(\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)/i;
+const RESETS_AFTER_RE = /resets? after (\d+h)?(\d+m)?(\d+s)?/i;
+const WILL_RESET_AFTER_RE = /will reset after (\d+h)?(\d+m)?(\d+s)?/i;
+const RESETS_IN_RE = /resets? in (\d+h)?(\d+m)?(\d+s)?/i;
+const RETRY_IN_SEC_RE = /please retry in (\d+(?:\.\d+)?)\s*s/i;
+const COOLDOWN_NUMERIC_RE = /^\d+(\.\d+)?$/;
export type RetryHintProvenance = "header" | "google_rpc_retry_info" | "body";
@@ -640,6 +653,9 @@ export async function recordCoreOwnedAntigravityQuotaState({
exactCooldownIsUpstreamReset: retryHintBypassesMaxCooldownMs(fallback.retryHintSource),
}
);
+ if (lockout.cooldownMs > 0 && isProviderExhaustedReason(fallback)) {
+ persistAntigravityFamilyCooldownIfQuota({ provider, connectionId, model, cooldownMs: lockout.cooldownMs, reason: "quota_exhausted" });
+ }
return { cooldownMs: lockout.cooldownMs, failureCount: lockout.failureCount };
}
@@ -1371,7 +1387,7 @@ export function parseRetryAfterFromBody(responseBody: unknown): {
// OpenAI: "Please retry after 20s" in message
const msg = String(error.message || body.message || "");
- const retryMatch = /retry\s+after\s+(\d+)\s*s/i.exec(msg);
+ const retryMatch = RETRY_AFTER_RE.exec(msg);
if (retryMatch) {
return {
retryAfterMs: Number.parseInt(retryMatch[1], 10) * 1000,
@@ -1404,16 +1420,13 @@ export function parseRetryFromErrorText(errorText: unknown): number | null {
// Gemini free-tier text fallback (no parseable JSON details present):
// "Please retry in 26.660853464s." Short throttle hint — capped independently of
// MAX_PROVIDER_COOLDOWN_MS, mirroring the JSON RetryInfo.retryDelay cap (#7940).
- const pleaseRetryMs = parseDelayString(/please retry in\s+([\d.]+\s*s)/i.exec(msg)?.[1]);
+ const pleaseRetryMs = parseDelayString(PLEASE_RETRY_RE.exec(msg)?.[1]);
if (pleaseRetryMs !== null && pleaseRetryMs > 0) {
return Math.min(pleaseRetryMs, MAX_SHORT_RETRY_HINT_MS);
}
// Issue #2321: parse embedded absolute ISO retry timestamps.
- const isoMatch =
- /\b(?:try again at|wait until|reset(?:s)? at|available at|retry after)\s+(\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)/i.exec(
- msg
- );
+ const isoMatch = ISO_RETRY_RE.exec(msg);
if (isoMatch) {
const parsedTs = Date.parse(isoMatch[1]);
if (Number.isFinite(parsedTs)) {
@@ -1422,21 +1435,21 @@ export function parseRetryFromErrorText(errorText: unknown): number | null {
}
}
- const match = /resets? after (\d+h)?(\d+m)?(\d+s)?/i.exec(msg);
+ const match = RESETS_AFTER_RE.exec(msg);
if (match?.[1] || match?.[2] || match?.[3]) return computeDurationMs(match);
// Variant without "reset after": "will reset after XhYmZs"
- const altMatch = /will reset after (\d+h)?(\d+m)?(\d+s)?/i.exec(msg);
+ const altMatch = WILL_RESET_AFTER_RE.exec(msg);
if (altMatch?.[1] || altMatch?.[2] || altMatch?.[3]) return computeDurationMs(altMatch);
// Antigravity / Cloud Code phrasing: "Resets in 164h27m24s".
- const resetsInMatch = /resets? in (\d+h)?(\d+m)?(\d+s)?/i.exec(msg);
+ const resetsInMatch = RESETS_IN_RE.exec(msg);
if (resetsInMatch?.[1] || resetsInMatch?.[2] || resetsInMatch?.[3]) {
return computeDurationMs(resetsInMatch);
}
// Gemini phrasing: "Please retry in 54.472178091s" (fractional seconds).
- const retryInSecMatch = /please retry in (\d+(?:\.\d+)?)\s*s/i.exec(msg);
+ const retryInSecMatch = RETRY_IN_SEC_RE.exec(msg);
if (retryInSecMatch?.[1]) {
const sec = Number.parseFloat(retryInSecMatch[1]);
if (Number.isFinite(sec) && sec > 0) {
@@ -1594,7 +1607,8 @@ export function isDailyQuotaExhausted(errorText: string): boolean {
return (
lower.includes("today's quota") ||
lower.includes("daily quota") ||
- lower.includes("try again tomorrow")
+ lower.includes("try again tomorrow") ||
+ lower.includes("tpd rate limit")
);
}
@@ -1640,7 +1654,12 @@ export function checkFallbackError(
headers: Headers | Record | null = null,
profileOverride: ProviderProfile | null = null,
structuredError?: { code?: string | null; type?: string | null } | null,
- rotation?: { account?: unknown } | null
+ rotation?: { account?: unknown } | null,
+ dailyReset?: {
+ timezone?: unknown;
+ hour?: unknown;
+ nowMs?: number;
+ } | null,
): {
shouldFallback: boolean;
cooldownMs: number;
@@ -1923,8 +1942,13 @@ export function checkFallbackError(
}
}
- // T10 (sub2api #1169) + #8247: credits/quota exhausted; *-compatible-* nicknames stay model-scoped.
- if (shouldUseQuotaSignal && isCreditsExhausted(errorStr) && !isCompatibleProvider(provider)) {
+ // T10 (sub2api #1169) + #8247: credits/quota exhausted; *-compatible-* nicknames stay model-scoped
+ // unless the body is an account-level Open Platform empty wallet.
+ if (
+ shouldUseQuotaSignal &&
+ isCreditsExhausted(errorStr) &&
+ (!isCompatibleProvider(provider) || isMoonshotAccountBalanceExhausted(errorStr))
+ ) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.paymentRequired ?? 3600 * 1000, // 1h cooldown
@@ -1933,17 +1957,43 @@ export function checkFallbackError(
};
}
- // Daily quota exhausted — lock model until tomorrow
+ // Daily quota exhausted. TPD uses the node clock / header; other daily
+ // quota text still uses getMsUntilTomorrow. TPD without either is not a
+ // host-midnight lock — fall through to short 429.
if (shouldUseQuotaSignal && isDailyQuotaExhausted(errorStr)) {
- const msUntilTomorrow = getMsUntilTomorrow();
- // Cap at 24 hours to handle timezone edge cases
- const cooldownMs = Math.min(msUntilTomorrow, 24 * 60 * 60 * 1000);
- return {
- shouldFallback: true,
- cooldownMs,
- reason: RateLimitReason.QUOTA_EXHAUSTED,
- dailyQuotaExhausted: true,
- };
+ if (isTpdRateLimit(errorStr)) {
+ const headerResetAtMs = parseResetFromHeaders(headers);
+ const tpdMs = resolveTpdCooldownMs(errorStr, {
+ timezone: dailyReset?.timezone,
+ hour: dailyReset?.hour,
+ nowMs: dailyReset?.nowMs,
+ headerResetAtMs,
+ });
+ if (tpdMs == null) {
+ // no clock, no header — short 429, do not guess midnight
+ console.warn(
+ "[accountFallback] TPD 429 without node daily-reset clock or Reset header; using short cooldown",
+ { provider },
+ );
+ } else {
+ return {
+ shouldFallback: true,
+ cooldownMs: tpdMs,
+ reason: RateLimitReason.QUOTA_EXHAUSTED,
+ dailyQuotaExhausted: true,
+ };
+ }
+ } else {
+ const msUntilTomorrow = getMsUntilTomorrow();
+ // Cap at 24 hours to handle timezone edge cases
+ const cooldownMs = Math.min(msUntilTomorrow, 24 * 60 * 60 * 1000);
+ return {
+ shouldFallback: true,
+ cooldownMs,
+ reason: RateLimitReason.QUOTA_EXHAUSTED,
+ dailyQuotaExhausted: true,
+ };
+ }
}
// Issue #2321 (5h subscription quota) + Issue #3709 (ollama-cloud weekly
@@ -2226,7 +2276,7 @@ export function cooldownUntilMs(value: string | number | Date | null | undefined
if (value instanceof Date) return value.getTime();
if (typeof value === "number") return value;
const raw = value.trim();
- if (/^\d+(\.\d+)?$/.test(raw)) return Number(raw);
+ if (COOLDOWN_NUMERIC_RE.test(raw)) return Number(raw);
return new Date(raw).getTime();
}
@@ -2382,12 +2432,7 @@ export function applyErrorState(
// (`markConnectionQuotaExhausted`) so a DB failure can never crash the
// chat path. See issue #1 (per-account 429 cascade not persisting).
const connId = (account as AccountState | null | undefined)?.id;
- if (
- typeof connId === "string" &&
- connId.length > 0 &&
- effectiveCooldownMs > 0 &&
- nextState.rateLimitedUntil
- ) {
+ if (typeof connId === "string" && connId.length > 0 && effectiveCooldownMs > 0 && nextState.rateLimitedUntil && !isAntigravityQuotaProvider(prov)) {
try {
const untilMs = cooldownUntilMs(nextState.rateLimitedUntil);
if (Number.isFinite(untilMs) && untilMs > Date.now()) {
diff --git a/open-sse/services/antigravityFamilyCooldown.ts b/open-sse/services/antigravityFamilyCooldown.ts
new file mode 100644
index 0000000000..975822a88f
--- /dev/null
+++ b/open-sse/services/antigravityFamilyCooldown.ts
@@ -0,0 +1,158 @@
+/**
+ * Persist Antigravity/agy quota cooldowns per model family (gemini vs claude)
+ * on the connection row, without cooling the whole account.
+ */
+import { lockModel } from "./accountFallback.ts";
+import {
+ getAntigravityQuotaFamily,
+ isAntigravityQuotaProvider,
+} from "./antigravityQuotaFamily.ts";
+
+type JsonRecord = Record;
+
+const FAMILY_PSD_KEY = "antigravityFamilyRateLimitedUntil";
+
+function asRecord(value: unknown): JsonRecord {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as JsonRecord)
+ : {};
+}
+
+function parseUntilMs(value: unknown): number {
+ if (typeof value === "number" && Number.isFinite(value)) return value;
+ if (typeof value === "string" && value.trim()) {
+ const ms = /^\d+(\.\d+)?$/.test(value.trim()) ? Number(value) : Date.parse(value);
+ return Number.isFinite(ms) ? ms : NaN;
+ }
+ return NaN;
+}
+
+function dummyModelForFamily(family: "gemini" | "claude"): string {
+ return family === "gemini" ? "gemini-family-lock" : "claude-family-lock";
+}
+
+function lockAntigravityFamilyModel(
+ connectionId: string,
+ model: string,
+ reason: string,
+ cooldownMs: number
+): void {
+ lockModel("agy", connectionId, model, reason, cooldownMs);
+ lockModel("antigravity", connectionId, model, reason, cooldownMs);
+}
+
+export async function persistAntigravityFamilyCooldown(params: {
+ connectionId: string;
+ model: string;
+ rateLimitedUntil: string;
+}): Promise {
+ if (!params.model.trim()) return null;
+ const family = getAntigravityQuotaFamily(params.model);
+ if (family === "other") return null;
+
+ const { getProviderConnectionById, updateProviderConnection } = await import(
+ "@/lib/db/providers"
+ );
+ const conn = (await getProviderConnectionById(params.connectionId)) as
+ | { provider?: string; providerSpecificData?: JsonRecord | null }
+ | null;
+ if (!conn || !isAntigravityQuotaProvider(conn.provider ?? null)) return null;
+
+ const psd = asRecord(conn.providerSpecificData);
+ const untils = asRecord(psd[FAMILY_PSD_KEY]);
+ const existingMs = parseUntilMs(untils[family]);
+ const nextMs = parseUntilMs(params.rateLimitedUntil);
+ if (!Number.isFinite(nextMs)) return psd;
+ if (Number.isFinite(existingMs) && existingMs > Date.now() && existingMs >= nextMs) {
+ return psd;
+ }
+
+ const nextPsd: JsonRecord = {
+ ...psd,
+ [FAMILY_PSD_KEY]: { ...untils, [family]: params.rateLimitedUntil },
+ };
+ await updateProviderConnection(params.connectionId, { providerSpecificData: nextPsd });
+ return nextPsd;
+}
+
+/** Fire-and-forget family PSD write. RPM/burst 429s must pass reason !== quota_exhausted. */
+export function persistAntigravityFamilyCooldownIfQuota(params: {
+ provider?: string | null;
+ connectionId: string;
+ model?: string | null;
+ cooldownMs: number;
+ reason?: string | null;
+}): void {
+ if (!isAntigravityQuotaProvider(params.provider)) return;
+ if (!params.model?.trim() || params.cooldownMs <= 0) return;
+ if (params.reason != null && params.reason !== "quota_exhausted") return;
+ void persistAntigravityFamilyCooldown({
+ connectionId: params.connectionId,
+ model: params.model,
+ rateLimitedUntil: new Date(Date.now() + params.cooldownMs).toISOString(),
+ }).catch(() => {});
+}
+
+export async function persistAntigravityPreflightFamilyLock(params: {
+ provider: string;
+ connectionId: string;
+ model: string;
+ unavailableUntil: string;
+}): Promise {
+ const cooldownMs = Math.max(0, Date.parse(params.unavailableUntil) - Date.now());
+ lockAntigravityFamilyModel(params.connectionId, params.model, "quota_exhausted", cooldownMs);
+ await persistAntigravityFamilyCooldown({
+ connectionId: params.connectionId,
+ model: params.model,
+ rateLimitedUntil: params.unavailableUntil,
+ });
+}
+
+export function rehydrateAntigravityFamilyLocks(
+ provider: string,
+ connectionId: string,
+ providerSpecificData: JsonRecord | null | undefined
+): void {
+ if (!isAntigravityQuotaProvider(provider)) return;
+ const untils = asRecord(asRecord(providerSpecificData)[FAMILY_PSD_KEY]);
+ const now = Date.now();
+ for (const family of ["gemini", "claude"] as const) {
+ const untilMs = parseUntilMs(untils[family]);
+ if (!Number.isFinite(untilMs) || untilMs <= now) continue;
+ const model = dummyModelForFamily(family);
+ const remainingMs = untilMs - now;
+ lockAntigravityFamilyModel(connectionId, model, "quota_exhausted", remainingMs);
+ }
+}
+
+export function rehydrateAntigravityFamilyLocksForConnections(
+ provider: string,
+ connections: Array<{ id: string; providerSpecificData?: unknown }>
+): void {
+ if (!isAntigravityQuotaProvider(provider)) return;
+ for (const conn of connections) {
+ rehydrateAntigravityFamilyLocks(
+ provider,
+ conn.id,
+ conn.providerSpecificData as JsonRecord | null | undefined
+ );
+ }
+}
+
+/** Family lock for executor quota exhaustion. Returns false when model is absent. */
+export function markAntigravityModelQuotaExhausted(
+ connectionId: string,
+ retryAfterMs: number,
+ model?: string | null
+): boolean {
+ if (!model) return false;
+ lockAntigravityFamilyModel(connectionId, model, "quota_exhausted", retryAfterMs);
+ persistAntigravityFamilyCooldownIfQuota({
+ provider: "agy",
+ connectionId,
+ model,
+ cooldownMs: retryAfterMs,
+ reason: "quota_exhausted",
+ });
+ return true;
+}
diff --git a/open-sse/services/antigravityQuotaFamily.ts b/open-sse/services/antigravityQuotaFamily.ts
index e9ede18749..94016c1109 100644
--- a/open-sse/services/antigravityQuotaFamily.ts
+++ b/open-sse/services/antigravityQuotaFamily.ts
@@ -54,3 +54,77 @@ export function getQuotaScopeLabelForProvider(
if (provider !== "antigravity" && provider !== "agy") return "model";
return getAntigravityQuotaFamily(model) === "other" ? "model" : "family";
}
+
+export function isAntigravityQuotaProvider(provider: string | null | undefined): boolean {
+ return provider === "antigravity" || provider === "agy";
+}
+
+export function quotaWindowNamesForScope(
+ names: string[],
+ scope?: { provider?: string | null; requestedModel?: string | null }
+): string[] {
+ if (!scope?.requestedModel || !isAntigravityQuotaProvider(scope.provider)) return names;
+ const scoped = selectAntigravityQuotaWindowNames(names, scope.requestedModel);
+ return scoped.length > 0 ? scoped : names;
+}
+
+/** Min remaining % across scoped windows, or 100 when an Antigravity family scope matched none. */
+export function remainingPercentFromQuotaWindows(
+ rawWindows: Record,
+ scope?: { provider?: string | null; requestedModel?: string | null }
+): number | null {
+ const names = Object.keys(rawWindows);
+ const namesToScan = quotaWindowNamesForScope(names, scope);
+ let minRemaining: number | null = null;
+ for (const name of namesToScan) {
+ const windowInfo = rawWindows[name];
+ if (!windowInfo || typeof windowInfo !== "object") continue;
+ const percentUsed = Number((windowInfo as Record).percentUsed);
+ if (!Number.isFinite(percentUsed)) continue;
+ const remaining = Math.max(0, Math.min(100, (1 - percentUsed) * 100));
+ minRemaining = minRemaining === null ? remaining : Math.min(minRemaining, remaining);
+ }
+ if (minRemaining !== null) return minRemaining;
+ if (scope?.requestedModel && namesToScan !== names) return 100;
+ return null;
+}
+
+/**
+ * Windows that belong to the requested Antigravity family. Claude weekly must
+ * not ride along on a Gemini request (and the reverse).
+ */
+export function selectAntigravityQuotaWindowNames(
+ quotaNames: string[],
+ requestedModel: string | null | undefined
+): string[] {
+ if (!requestedModel) return quotaNames;
+ const requestedFamily = getAntigravityQuotaFamily(requestedModel);
+ const cleanRequestedModel = requestedModel.replace(/^(antigravity|agy)\//, "");
+ const bareModel = cleanRequestedModel.includes("/")
+ ? cleanRequestedModel.slice(cleanRequestedModel.lastIndexOf("/") + 1)
+ : cleanRequestedModel;
+
+ if (requestedFamily === "other") {
+ return quotaNames.filter((windowName) => {
+ const bare = windowName.replace(/^(antigravity|agy)\//, "");
+ return bare === bareModel || bare === cleanRequestedModel;
+ });
+ }
+
+ const familyAggregates =
+ requestedFamily === "gemini"
+ ? ["gemini_weekly"]
+ : requestedFamily === "claude"
+ ? ["claude_gpt_weekly"]
+ : [];
+
+ const exactWindows = quotaNames.filter((windowName) => {
+ const bare = windowName.replace(/^(antigravity|agy)\//, "");
+ return bare === bareModel;
+ });
+ const aggregateWindows = familyAggregates.filter((key) => quotaNames.includes(key));
+ const scoped = [...exactWindows, ...aggregateWindows];
+ if (scoped.length > 0) return scoped;
+
+ return quotaNames.filter((windowName) => getAntigravityQuotaFamily(windowName) === requestedFamily);
+}
diff --git a/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts b/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts
index b1d8d31b36..b3f719558f 100644
--- a/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts
+++ b/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts
@@ -59,11 +59,9 @@ describe("#11489 resolveScoresAs", () => {
expect(resolveScoresAs("claude-sonnet-5")).toEqual({ base: "claude-sonnet-5", via: null });
});
- it("resolves the cursor/agy spelling of a Claude model to its canonical id", () => {
- // `claude-4.6-opus-high` strips to `claude-4.6-opus`, which is not a catalog
- // id — the canonical spelling is `claude-opus-4-6`. Explicit registry data.
- expect(resolveScoresAs("claude-4.6-opus-high")).toEqual({
- base: "claude-opus-4-6",
+ it("resolves curated Cursor Claude variants to their canonical ids", () => {
+ expect(resolveScoresAs("claude-fable-5-1-thinking-high")).toEqual({
+ base: "claude-fable-5-1",
via: "explicit",
});
expect(resolveScoresAs("claude-4.6-sonnet-medium")).toEqual({
diff --git a/open-sse/services/browserBackedChat.ts b/open-sse/services/browserBackedChat.ts
index 4b3c7078e7..fa51a9c266 100644
--- a/open-sse/services/browserBackedChat.ts
+++ b/open-sse/services/browserBackedChat.ts
@@ -236,6 +236,7 @@ export async function browserBackedChat(
userAgent,
locale,
timezone,
+ headless,
inputSelector,
submitButtonSelector,
submitButtonMode = "playwright",
@@ -257,11 +258,13 @@ export async function browserBackedChat(
userAgent,
locale,
timezone,
+ headless,
});
const acquireContextMs = Date.now() - tAcquireStart;
const page = await openPage(pooled);
const observedPostUrls: string[] = [];
+ const observedPostResponses: Array<{ url: string; status: number }> = [];
page.on("request", (request) => {
if (request.method() !== "POST") return;
try {
@@ -273,6 +276,19 @@ export async function browserBackedChat(
// Ignore malformed/non-HTTP request URLs.
}
});
+ page.on("response", (response) => {
+ if (response.request().method() !== "POST") return;
+ try {
+ const url = new URL(response.url());
+ if (!url.hostname.endsWith(chatUrlMatchDomain)) return;
+ observedPostResponses.push({
+ url: `${url.origin}${url.pathname}`,
+ status: response.status(),
+ });
+ } catch {
+ // Ignore malformed/non-HTTP response URLs.
+ }
+ });
try {
const tNavStart = Date.now();
await withAbort(
@@ -379,6 +395,7 @@ export async function browserBackedChat(
body,
isStealth: pooled.isStealth,
observedPostUrls,
+ observedPostResponses,
timing: {
acquireContextMs,
navigateMs,
@@ -404,6 +421,7 @@ export async function browserBackedChat(
body,
isStealth: pooled.isStealth,
observedPostUrls,
+ observedPostResponses,
timing: {
acquireContextMs,
navigateMs: 0,
diff --git a/open-sse/services/browserBackedChat/types.ts b/open-sse/services/browserBackedChat/types.ts
index c90fb3b80e..3a197be16c 100644
--- a/open-sse/services/browserBackedChat/types.ts
+++ b/open-sse/services/browserBackedChat/types.ts
@@ -29,6 +29,8 @@ export interface BrowserBackedChatRequest {
locale?: string;
/** Browser IANA timezone. Defaults to America/New_York. */
timezone?: string;
+ /** Launch a headed browser when the provider rejects true headless mode. */
+ headless?: boolean;
/** Selector for the provider chat input. */
inputSelector: string;
/** Optional selector for the provider submit button. */
@@ -64,6 +66,8 @@ export interface BrowserBackedChatResult {
isStealth: boolean;
/** Sanitized POST targets observed while submitting. */
observedPostUrls?: string[];
+ /** Sanitized POST response targets and statuses observed while submitting. */
+ observedPostResponses?: Array<{ url: string; status: number }>;
timing: {
acquireContextMs: number;
navigateMs: number;
diff --git a/open-sse/services/browserPool.ts b/open-sse/services/browserPool.ts
index bcab2dc174..09f1d40866 100644
--- a/open-sse/services/browserPool.ts
+++ b/open-sse/services/browserPool.ts
@@ -26,6 +26,8 @@
import { Buffer } from "node:buffer";
+import { connectObscuraBrowser } from "./obscura.ts";
+
type Browser = import("playwright").Browser;
type BrowserContext = import("playwright").BrowserContext;
type Page = import("playwright").Page;
@@ -86,11 +88,20 @@ function createBrowserPoolMetrics(): BrowserPoolMetrics {
};
}
+type PoolEngine = "obscura" | "cloakbrowser" | "chromium";
+
+interface PendingContextEntry {
+ promise: Promise;
+ createdAt: number;
+}
+
interface PoolState {
browser: Browser | null;
+ /** Engine backing the headless browser, for metrics and stealth detection. */
+ engine: PoolEngine | null;
headedBrowser: Browser | null;
contexts: Map;
- pendingContexts: Map>;
+ pendingContexts: Map;
launching: Promise | null;
headedLaunching: Promise | null;
generation: number;
@@ -110,9 +121,10 @@ const DEFAULT_USER_AGENT =
const state: PoolState = {
browser: null,
+ engine: null,
headedBrowser: null,
contexts: new Map(),
- pendingContexts: new Map(),
+ pendingContexts: new Map; createdAt: number }>(),
launching: null,
headedLaunching: null,
generation: 0,
@@ -175,6 +187,15 @@ function evictStaleContexts(): void {
pooled.context.close().catch(() => {});
}
}
+ // #12179: also evict pendingContexts entries that never resolved, so a hung
+ // launch cannot pin the map (and the pool) open forever.
+ const PENDING_TTL_MS = 5 * 60 * 1000;
+ for (const [key, pending] of state.pendingContexts) {
+ if (now - pending.createdAt > PENDING_TTL_MS) {
+ state.pendingContexts.delete(key);
+ state.metrics.contextsEvicted++;
+ }
+ }
if (
state.contexts.size === 0 &&
state.pendingContexts.size === 0 &&
@@ -288,13 +309,26 @@ async function launchBrowserInstance(
options: BrowserPoolContextOptions,
headless: boolean
): Promise {
+ // A headed browser must be a real windowed Chromium, so the engine
+ // preference below applies to the headless path only.
if (!headless) {
const { chromium } = await import("playwright");
return chromium.launch(resolvePlainBrowserLaunchOptions(options));
}
+ // #12274: prefer Obscura (lightweight, browser-grade CDP) over a full
+ // Chromium; fall back to cloakbrowser, then plain Chromium. Obscura's
+ // lifecycle (one shared `obscura serve` per process) lives in ./obscura.ts,
+ // so executors like cloudflare-playground reuse the same server.
+ const obscura = await connectObscuraBrowser();
+ if (obscura) {
+ state.engine = "obscura";
+ return obscura.browser;
+ }
+
const cloakLaunch = await resolveCloakLaunch();
if (cloakLaunch) {
+ state.engine = "cloakbrowser";
return cloakLaunch({
headless: true,
args: ["--no-sandbox", "--disable-dev-shm-usage"],
@@ -303,6 +337,7 @@ async function launchBrowserInstance(
// Fallback: plain Playwright. Works for Claude web (cookie-only auth) but
// DDG's VQD challenge will detect this Chromium build.
+ state.engine = "chromium";
const { chromium } = await import("playwright");
return chromium.launch(resolvePlainBrowserLaunchOptions(options));
}
@@ -464,14 +499,14 @@ export async function acquireBrowserContext(
// Dedup concurrent creations for the same key
const pending = state.pendingContexts.get(poolKey);
- if (pending) return pending;
+ if (pending) return pending.promise;
const createPromise = (async (): Promise => {
const [browser, proxy] = await Promise.all([
launchBrowser(options),
resolveBrowserContextProxy(key, options),
]);
- const isStealth = headless && state.cloakLaunch !== null;
+ const isStealth = headless && (state.engine === "obscura" || state.cloakLaunch !== null);
const context = await browser.newContext({
userAgent: options.userAgent || DEFAULT_USER_AGENT,
locale: options.locale || "en-US",
@@ -510,7 +545,7 @@ export async function acquireBrowserContext(
return pooled;
})();
- state.pendingContexts.set(poolKey, createPromise);
+ state.pendingContexts.set(poolKey, { promise: createPromise, createdAt: Date.now() });
createPromise
.then(() => settlePendingContext(poolKey, false))
.catch(() => settlePendingContext(poolKey, true));
@@ -580,6 +615,10 @@ export async function shutdownPool(reason: string): Promise {
}
state.launching = null;
state.headedLaunching = null;
+ // #12274: the shared Obscura server is owned by ./obscura.ts and reused by
+ // executors (cloudflare-playground), so closing the pool's CDP connection is
+ // enough — never kill the server here.
+ state.engine = null;
state.lastActivity = Date.now();
// Avoid unused-parameter lint: log reason via debug if anyone hooks
// process.on('exit') and prints state.
@@ -590,6 +629,7 @@ export function getBrowserPoolStatus(): {
enabled: boolean;
contexts: number;
browserRunning: boolean;
+ engine: PoolEngine | null;
stealthAvailable: boolean;
lastActivityAgoMs: number;
} {
@@ -597,7 +637,8 @@ export function getBrowserPoolStatus(): {
enabled: isPoolEnabled(),
contexts: state.contexts.size,
browserRunning: state.browser !== null || state.headedBrowser !== null,
- stealthAvailable: state.cloakLaunch !== null,
+ engine: state.engine,
+ stealthAvailable: state.engine === "obscura" || state.cloakLaunch !== null,
lastActivityAgoMs: state.lastActivity === 0 ? -1 : Date.now() - state.lastActivity,
};
}
diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts
index 9c316e52c7..be662c600f 100644
--- a/open-sse/services/combo.ts
+++ b/open-sse/services/combo.ts
@@ -613,12 +613,13 @@ export async function buildAutoCandidates(
const quota = await quotaPromises.get(quotaKey)!;
resetWindowAffinity = calculateResetWindowAffinity(quota, resetWindowConfig);
if (!quotaCutoffBlocked) {
- quotaRemaining = quotaRemainingPercentFromQuota(quota);
+ quotaRemaining = quotaRemainingPercentFromQuota(quota, { provider, requestedModel: modelStr });
}
if (!quotaCutoffBlocked && quotaCutoffEnabled) {
const cutoffDecision = evaluateQuotaCutoff(
quota as QuotaInfo | null,
- buildAutoQuotaThresholds(provider, connection, resilienceSettings)
+ buildAutoQuotaThresholds(provider, connection, resilienceSettings),
+ { provider, requestedModel: modelStr }
);
if (!cutoffDecision.proceed) {
quotaCutoffBlocked = true;
@@ -1379,7 +1380,7 @@ async function handleComboChatInner({
resilienceSettings,
quotaCutoffResetWindowConfig,
combo.name,
- log
+ log, modelStr
);
if (quotaCutoff.blocked) {
log.info(
@@ -1660,8 +1661,16 @@ async function handleComboChatInner({
}
}
- // Universal handoff: inject existing handoff if model changed
+ // Universal handoff: inject existing handoff if model changed. i === 0
+ // only: a fallback target (i > 0) serves the SAME client request the
+ // failed primary target would have served, with the original messages
+ // already intact -- there's nothing to hand off, since the client never
+ // saw the earlier target fail. Injecting a handoff note there replaces
+ // real context with a context-free note, which weaker fallback models
+ // have been observed treating as license to fabricate content instead
+ // of just answering the actual request (#12227 follow-up).
if (
+ i === 0 &&
universalHandoffConfig.enabled &&
relayOptions?.sessionId &&
!(body as Record)?.[SKIP_UNIVERSAL_HANDOFF_FLAG]
@@ -1927,7 +1936,14 @@ async function handleComboChatInner({
provider,
target.connectionId ?? undefined
);
- if (prevModel && prevModel !== modelStr) {
+ // i === 0 only: a same-request fallback target (i > 0) never
+ // needs a summary generated for it -- see the injection-site
+ // comment above. recordSessionModelUsage above stays
+ // unconditional regardless of i: it must reflect whichever
+ // model actually served THIS response, since the next
+ // request's i === 0 comparison depends on that being
+ // accurate even when this response came from a fallback.
+ if (i === 0 && prevModel && prevModel !== modelStr) {
const handoffSourceMessages =
Array.isArray(body?.messages) && body.messages.length > 0
? body.messages
@@ -4015,8 +4031,5 @@ async function handleRoundRobinCombo({
}
log.warn("COMBO-RR", `All models failed | ${msg}`);
- return new Response(JSON.stringify({ error: { message: msg } }), {
- status,
- headers: { "Content-Type": "application/json" },
- });
+ return new Response(JSON.stringify({ error: { message: msg } }), { status, headers: { "Content-Type": "application/json" } });
}
diff --git a/open-sse/services/combo/comboPredicates.ts b/open-sse/services/combo/comboPredicates.ts
index 2c6acb099e..972abc5765 100644
--- a/open-sse/services/combo/comboPredicates.ts
+++ b/open-sse/services/combo/comboPredicates.ts
@@ -7,6 +7,7 @@
*/
import { EXECUTOR_CONTRACT_VIOLATION_CODE } from "../../config/constants.ts";
+import { remainingPercentFromQuotaWindows } from "../antigravityQuotaFamily.ts";
import { errorResponse } from "../../utils/error.ts";
import { parseModel } from "../model.ts";
import { isSelfInflictedUpstreamTimeout } from "../../handlers/chatCore/cooldownClassification.ts";
@@ -431,24 +432,24 @@ export function clampPercent(value: number): number {
return Math.max(0, Math.min(100, value));
}
-export function quotaRemainingPercentFromQuota(quota: unknown): number {
+export function quotaRemainingPercentFromQuota(
+ quota: unknown,
+ scope?: { provider?: string | null; requestedModel?: string | null }
+): number {
if (!quota || typeof quota !== "object") return 100;
const record = quota as Record;
- if (record.limitReached === true) return 0;
const windows = record.windows;
if (windows && typeof windows === "object" && !Array.isArray(windows)) {
- let minRemaining: number | null = null;
- for (const windowInfo of Object.values(windows as Record)) {
- if (!windowInfo || typeof windowInfo !== "object") continue;
- const percentUsed = Number((windowInfo as Record).percentUsed);
- if (!Number.isFinite(percentUsed)) continue;
- const remaining = clampPercent((1 - percentUsed) * 100);
- minRemaining = minRemaining === null ? remaining : Math.min(minRemaining, remaining);
- }
- if (minRemaining !== null) return minRemaining;
+ const fromWindows = remainingPercentFromQuotaWindows(
+ windows as Record,
+ scope
+ );
+ if (fromWindows !== null) return fromWindows;
}
+ if (record.limitReached === true) return 0;
+
const percentUsed = Number(record.percentUsed);
if (Number.isFinite(percentUsed)) return clampPercent((1 - percentUsed) * 100);
return 100;
diff --git a/open-sse/services/combo/comboStructure.ts b/open-sse/services/combo/comboStructure.ts
index 137561fbaa..9065e123b2 100644
--- a/open-sse/services/combo/comboStructure.ts
+++ b/open-sse/services/combo/comboStructure.ts
@@ -807,16 +807,48 @@ export function filterTargetsByRequestCompatibility(
return [];
}
+ // #12273: a sole survivor whose catalog window is known-too-small is a
+ // guaranteed context_length_exceeded. Restore the remaining pool so combo.ts
+ // can still try larger-context targets. Unknown context (`null`) is advisory
+ // and must not resurrect hard-rejected targets (vision / output / tools).
+ if (
+ compatible.length === 1 &&
+ (targetReasons.get(compatible[0]) || []).includes("context_window")
+ ) {
+ // #8332: never restore a confirmed-non-vision target onto an image request.
+ const restored = requirements.requiresVision
+ ? targets.filter((target) => !isVisionIncompatibleTarget(target, requirements))
+ : targets;
+ if (restored.length > compatible.length) {
+ log.warn(
+ "COMBO",
+ `${label}: single compatible target ${compatible[0].modelStr} has known context too small for ${requirements.requiredContextTokens} token request; falling back to full pool (#12273)`
+ );
+ return restored;
+ }
+ }
+
log.info(
"COMBO",
`${label}: kept ${compatible.length}/${targets.length} targets for request requirements`
);
- log.debug?.(
- "COMBO",
- `${label}: rejected targets ${rejected
- .map((entry) => `${entry.target.modelStr}(${entry.reasons.join("+")})`)
- .join(", ")}`
- );
+ // #12273: When pool collapses significantly, log rejection reasons at info
+ // level so the cause is diagnosable without enabling debug logging.
+ if (compatible.length <= 2 && targets.length > 4) {
+ log.info(
+ "COMBO",
+ `${label}: rejected targets ${rejected
+ .map((entry) => `${entry.target.modelStr}(${entry.reasons.join("+")})`)
+ .join(", ")}`
+ );
+ } else {
+ log.debug?.(
+ "COMBO",
+ `${label}: rejected targets ${rejected
+ .map((entry) => `${entry.target.modelStr}(${entry.reasons.join("+")})`)
+ .join(", ")}`
+ );
+ }
return compatible;
}
diff --git a/open-sse/services/combo/nativeCodexTurnPin.ts b/open-sse/services/combo/nativeCodexTurnPin.ts
index 9910d8d993..2b6d94945f 100644
--- a/open-sse/services/combo/nativeCodexTurnPin.ts
+++ b/open-sse/services/combo/nativeCodexTurnPin.ts
@@ -245,7 +245,8 @@ export async function isPinnedTargetModelScopedUnusable(args: {
resilienceSettings,
quotaCutoffResetWindowConfig,
comboName,
- log ?? { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
+ log ?? { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
+ target.modelStr
);
if (cutoff.blocked) return true;
}
diff --git a/open-sse/services/combo/quotaExhaustionCutoff.ts b/open-sse/services/combo/quotaExhaustionCutoff.ts
index 2dc78dbe1b..a73d3628f4 100644
--- a/open-sse/services/combo/quotaExhaustionCutoff.ts
+++ b/open-sse/services/combo/quotaExhaustionCutoff.ts
@@ -97,7 +97,8 @@ export async function resolveQuotaExhaustionCutoffForTarget(
resilienceSettings: ResilienceSettings | null | undefined,
resetWindowConfig: ResetWindowConfig,
comboName: string,
- log: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void }
+ log: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void },
+ requestedModel?: string | null
): Promise<{ blocked: boolean; reason?: string }> {
const quotaCutoffEnabled =
(resilienceSettings ?? resolveResilienceSettings(null))?.quotaPreflight?.enabled === true;
@@ -126,7 +127,8 @@ export async function resolveQuotaExhaustionCutoffForTarget(
});
const cutoffDecision = evaluateQuotaCutoff(
quota as QuotaInfo | null,
- buildAutoQuotaThresholds(provider, connection, resilienceSettings)
+ buildAutoQuotaThresholds(provider, connection, resilienceSettings),
+ { provider, requestedModel: requestedModel ?? null }
);
if (!cutoffDecision.proceed) {
return { blocked: true, reason: cutoffDecision.reason || "quota_exhausted" };
diff --git a/open-sse/services/combo/validateQuality.ts b/open-sse/services/combo/validateQuality.ts
index be70038994..723a10b614 100644
--- a/open-sse/services/combo/validateQuality.ts
+++ b/open-sse/services/combo/validateQuality.ts
@@ -16,6 +16,45 @@ import { evaluateResponseValidation, type ResponseValidationConfig } from "./res
import { getReasoningTokens } from "../../../src/lib/usage/tokenAccounting.ts";
import type { ComboRetryAfter } from "./types.ts";
+/**
+ * Detects tool_calls entries within one assistant message that repeat the
+ * exact same function name + arguments verbatim -- always a bug (no
+ * legitimate use calls one tool twice with identical arguments in the same
+ * turn), and a real observed failure mode of at least one free-tier
+ * streaming model (minimax-m3:free via OpenRouter/GMICloud, 2026-09-02:
+ * duplicated a heartbeat_respond call byte-for-byte, confirmed at the raw
+ * SSE wire level -- an upstream bug, not an OmniRoute reconstruction
+ * artifact). Used two ways: to fail a non-streaming response over to a
+ * sibling combo target (see validateResponseQuality below), and, post-
+ * stream, to flag an already-relayed streaming response as an on-spec
+ * violation despite its clean HTTP 200 (see attemptLogging.ts's
+ * persistAttemptLogs) -- a streaming response can't be retried once real
+ * content has started reaching the client (the quality-gate peek below only
+ * ever validates the START of a stream, by design, to avoid buffering the
+ * whole response and defeating streaming's latency purpose), so flagging it
+ * after the fact is what's actually achievable for that path.
+ */
+export function findToolCallSpecViolation(responseBody: unknown): string | null {
+ const json = isRecord(responseBody) ? responseBody : null;
+ const choices = json?.choices;
+ const firstChoice = Array.isArray(choices) ? choices[0] : null;
+ const message = isRecord(firstChoice) ? firstChoice.message : null;
+ const toolCalls = isRecord(message) ? message.tool_calls : null;
+ if (!Array.isArray(toolCalls) || toolCalls.length < 2) return null;
+
+ const seen = new Set();
+ for (const call of toolCalls) {
+ const fn = isRecord(call) ? call.function : null;
+ if (!isRecord(fn) || typeof fn.name !== "string" || typeof fn.arguments !== "string") {
+ continue;
+ }
+ const signature = `${fn.name}\u0000${fn.arguments}`;
+ if (seen.has(signature)) return `duplicate tool_calls entry for "${fn.name}"`;
+ seen.add(signature);
+ }
+ return null;
+}
+
export function toRetryAfterDisplayValue(value: ComboRetryAfter): string | Date {
if (typeof value !== "number") return value;
if (value > 0 && value < 1_000_000_000) {
@@ -327,9 +366,9 @@ export async function validateResponseQuality(
function isTerminalUsageOnlyChunk(parsed: Record, eventType: string): boolean {
return Boolean(
parsed.usage &&
- typeof parsed.usage === "object" &&
- !Array.isArray(parsed.choices) &&
- !eventType.startsWith("response.")
+ typeof parsed.usage === "object" &&
+ !Array.isArray(parsed.choices) &&
+ !eventType.startsWith("response.")
);
}
@@ -734,6 +773,11 @@ export async function validateResponseQuality(
}
const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
+ const specViolation = findToolCallSpecViolation(json);
+ if (specViolation) {
+ return { valid: false, reason: specViolation };
+ }
+
if (!hasContent && !hasToolCalls) {
return { valid: false, reason: "empty content and no tool_calls in response" };
}
diff --git a/open-sse/services/compression/resultMemo.ts b/open-sse/services/compression/resultMemo.ts
index b4c64d9112..de198d1043 100644
--- a/open-sse/services/compression/resultMemo.ts
+++ b/open-sse/services/compression/resultMemo.ts
@@ -149,7 +149,7 @@ export function memoLookup(key: string): CompressionResult | null {
memoHits++;
recordLookup(true);
// Return a clone so downstream mutation cannot corrupt the cached value.
- const cloned = JSON.parse(JSON.stringify(hit)) as CompressionResult;
+ const cloned = structuredClone(hit);
if (cloned.stats) {
cloned.stats.memoHit = true;
}
@@ -162,7 +162,7 @@ export function memoStore(key: string, result: CompressionResult): CompressionRe
// Returns the stored clone so callers that need a fresh instance (the common
// `memoStore(key, result); return memoLookup(key)!` idiom) can avoid a redundant
// second multi-MB deep clone of the body on the way out.
- const stored = JSON.parse(JSON.stringify(result)) as CompressionResult;
+ const stored = structuredClone(result);
boundedSet(key, stored);
return stored;
}
diff --git a/open-sse/services/contextHandoff.ts b/open-sse/services/contextHandoff.ts
index bd169be606..c43b7f55c1 100644
--- a/open-sse/services/contextHandoff.ts
+++ b/open-sse/services/contextHandoff.ts
@@ -407,7 +407,10 @@ async function generateHandoffAsync(options: {
relayConfig.relayMode
);
const historyText = formatMessagesForPrompt(selectedMessages);
- if (!historyText) return;
+ if (!historyText) {
+ logUniversalHandoffOutcome("unavailable", options.comboName, "empty selected-message history");
+ return;
+ }
const summaryPrompt = HANDOFF_PROMPT_TEMPLATE.replace("{HISTORY}", historyText);
const summaryBody = {
@@ -421,7 +424,14 @@ async function generateHandoffAsync(options: {
};
const response = await options.handleSingleModel(summaryBody, summaryModel);
- if (!response.ok) return;
+ if (!response.ok) {
+ logUniversalHandoffOutcome(
+ "unavailable",
+ options.comboName,
+ `summary model call failed: status=${response.status} model=${summaryModel}`
+ );
+ return;
+ }
let content = "";
try {
@@ -436,7 +446,14 @@ async function generateHandoffAsync(options: {
}
const parsed = parseHandoffJSON(content);
- if (!parsed) return;
+ if (!parsed) {
+ logUniversalHandoffOutcome(
+ "unparseable",
+ options.comboName,
+ `model=${summaryModel} contentPreview=${JSON.stringify(content.slice(0, 200))}`
+ );
+ return;
+ }
upsertHandoff({
sessionId: options.sessionId,
@@ -572,7 +589,7 @@ export function buildUniversalHandoffSystemMessage(
${escapedReason}
${escapedPrev}
${escapedCurr}
-A continuación se resume toda la conversacion para continuar sin perder el hilo.
+No prior-session summary is available for this handoff. The input below (e.g. a tool result) is the entire context you have -- do not assume or invent details about a broader conversation you cannot see.
`;
}
@@ -687,6 +704,23 @@ export function resetUniversalHandoffCooldowns(): void {
universalHandoffCooldowns.clear();
}
+// Every non-"generated" outcome across both handoff generators (this one and
+// the older generateHandoffAsync above) used to be silent -- context_handoffs
+// staying empty gave no signal on WHY (upstream call failing vs. malformed
+// output vs. no history to summarize). Every live handoff then falls back to
+// the bare no-summary note (buildUniversalHandoffSystemMessage's `!payload`
+// branch / the context-relay equivalent), which is what actually reaches the
+// model/user; without this log that always reads as a mystery instead of a
+// traceable cause.
+function logUniversalHandoffOutcome(
+ outcome: "unavailable" | "unparseable",
+ comboName: string,
+ detail: string
+): void {
+ if (process.env.NODE_ENV === "test") return;
+ console.warn(`[universal-handoff] ${outcome} (combo=${comboName}): ${detail}`);
+}
+
/**
* Generate a universal handoff summary for any model/provider switch.
*/
@@ -709,7 +743,10 @@ async function generateUniversalHandoffAsync(options: {
options.relayMode
);
const historyText = formatMessagesForPrompt(selectedMessages);
- if (!historyText) return "unavailable";
+ if (!historyText) {
+ logUniversalHandoffOutcome("unavailable", options.comboName, "empty selected-message history");
+ return "unavailable";
+ }
const summaryPrompt = HANDOFF_PROMPT_TEMPLATE.replace("{HISTORY}", historyText);
const summaryModel = options.handoffModel || options.currModel;
@@ -735,22 +772,25 @@ async function generateUniversalHandoffAsync(options: {
};
const response = await options.handleSingleModel(summaryBody, summaryModel);
- if (!response.ok) return "unavailable";
+ if (!response.ok) {
+ const detail = `summary model call failed: status=${response.status} model=${summaryModel}`;
+ logUniversalHandoffOutcome("unavailable", options.comboName, detail);
+ return "unavailable";
+ }
let content = "";
try {
- const json = (await response.clone().json()) as Record;
- content = getResponseText(json);
+ content = getResponseText((await response.clone().json()) as Record);
} catch {
- try {
- content = await response.clone().text();
- } catch {
- content = "";
- }
+ content = await response.clone().text().catch(() => "");
}
const parsed = parseHandoffJSON(content);
- if (!parsed) return "unparseable";
+ if (!parsed) {
+ const preview = JSON.stringify(content.slice(0, 200));
+ logUniversalHandoffOutcome("unparseable", options.comboName, `model=${summaryModel} contentPreview=${preview}`);
+ return "unparseable";
+ }
upsertHandoff({
sessionId: options.sessionId,
diff --git a/open-sse/services/conversationTurnContent.ts b/open-sse/services/conversationTurnContent.ts
index a95a39c939..bc5f3bdcef 100644
--- a/open-sse/services/conversationTurnContent.ts
+++ b/open-sse/services/conversationTurnContent.ts
@@ -23,6 +23,90 @@ export type TurnDisplayContent = {
toolName: string | null;
};
+type CanonicalTurnLike = {
+ role: "system" | "user" | "assistant" | "tool";
+ text: string;
+ blockKind: "text" | "tool_use" | "tool_result";
+ toolName: string | null;
+};
+
+type JsonRecord = Record;
+
+function asRecord(value: unknown): JsonRecord | null {
+ return value && typeof value === "object" ? (value as JsonRecord) : null;
+}
+
+function turnsFromBody(body: unknown): CanonicalTurnLike[] {
+ const rec = asRecord(body);
+ return rec ? extractCanonicalTurns(rec) : [];
+}
+
+/**
+ * extractCanonicalTurns's Chat Completions branch only reads a message's
+ * `content` -- a tool-calling assistant message carries its call in
+ * `tool_calls` instead with `content: null`, so it silently produces no turn
+ * at all and the matching conversation_turn_nodes row can never resolve.
+ * Deliberately scoped to this read-only display path instead of extending
+ * extractCanonicalTurns itself: that function also drives
+ * conversationTracker.ts's write-path identity/hashing, and this codebase's
+ * only caller of it there (chat.ts's resolveConversationId) always feeds the
+ * client-facing Responses-API body -- never Chat Completions
+ * `messages`/`tool_calls` -- so extending it there would be unreachable for
+ * real traffic here but still carries real write-path identity-hash risk for
+ * any other caller/format that function might ever serve. Mirrors
+ * extractCanonicalTurns's own Responses-shape function_call handling: one
+ * turn per call, role "tool" (matches how a Responses API function_call item,
+ * which also carries no `role`, canonicalizes -- not "assistant"), toolName
+ * from the call, text the raw arguments string untouched (already a JSON
+ * string in both APIs, so passing it through unmodified is what a
+ * byte-identical hash against the original Responses-shaped item needs).
+ */
+function extractChatCompletionsToolUseTurns(messages: unknown): CanonicalTurnLike[] {
+ if (!Array.isArray(messages)) return [];
+ const turns: CanonicalTurnLike[] = [];
+ for (const item of messages) {
+ const rec = asRecord(item) ?? {};
+ if (rec.role !== "assistant" || !Array.isArray(rec.tool_calls)) continue;
+ for (const call of rec.tool_calls) {
+ const fn = asRecord(asRecord(call)?.function);
+ const args = fn?.arguments;
+ if (typeof args !== "string" || !args) continue;
+ turns.push({
+ role: "tool",
+ text: args,
+ blockKind: "tool_use",
+ toolName: typeof fn?.name === "string" ? fn.name : null,
+ });
+ }
+ }
+ return turns;
+}
+
+function turnsFromClientResponse(clientResponse: unknown): CanonicalTurnLike[] {
+ const rec = asRecord(clientResponse);
+ if (!rec) return [];
+ const summary = asRecord(rec.summary);
+ const output = Array.isArray(rec.output) ? rec.output : summary?.output;
+ return Array.isArray(output) ? extractCanonicalTurns({ input: output }) : [];
+}
+
+function turnsFromProviderRequest(body: unknown): CanonicalTurnLike[] {
+ const rec = asRecord(body);
+ return [...turnsFromBody(rec), ...extractChatCompletionsToolUseTurns(rec?.messages)];
+}
+
+function indexTurns(result: Map, turns: CanonicalTurnLike[]): void {
+ for (const turn of turns) {
+ const hash = hashTurnContent(turn);
+ if (result.has(hash)) continue;
+ result.set(hash, {
+ textPreview: turn.text,
+ blockKind: turn.blockKind,
+ toolName: turn.toolName,
+ });
+ }
+}
+
/**
* Resolve display content for a batch of turn nodes, keyed by content_hash.
* Content_hash is sha256(role+text) only — real traffic has plenty of
@@ -64,19 +148,10 @@ export function resolveTurnDisplayContent(
for (const relPath of artifactPathByCorrelationId.values()) {
const { artifact, state } = readCallArtifact(relPath);
if (state !== "ready") continue;
- const clientRawRequest = artifact?.pipeline?.clientRawRequest as { body?: unknown } | undefined;
- const body = clientRawRequest?.body;
- if (!body || typeof body !== "object") continue;
-
- for (const turn of extractCanonicalTurns(body as Record)) {
- const hash = hashTurnContent(turn);
- if (result.has(hash)) continue;
- result.set(hash, {
- textPreview: turn.text,
- blockKind: turn.blockKind,
- toolName: turn.toolName,
- });
- }
+ const pipeline = asRecord(artifact?.pipeline);
+ indexTurns(result, turnsFromBody(asRecord(pipeline?.clientRawRequest)?.body));
+ indexTurns(result, turnsFromClientResponse(pipeline?.clientResponse));
+ indexTurns(result, turnsFromProviderRequest(asRecord(pipeline?.providerRequest)?.body));
}
return result;
}
diff --git a/open-sse/services/dailyQuotaReset.ts b/open-sse/services/dailyQuotaReset.ts
new file mode 100644
index 0000000000..a5108d132f
--- /dev/null
+++ b/open-sse/services/dailyQuotaReset.ts
@@ -0,0 +1,145 @@
+/**
+ * Node-level daily quota reset clock.
+ *
+ * TPD cooldown endpoint: operator-configured IANA timezone + local hour.
+ * No default timezone. Do not call getMsUntilTomorrow() from here.
+ */
+
+export function isValidIanaTimeZone(tz: string): boolean {
+ if (typeof tz !== "string" || tz.trim() === "") return false;
+ try {
+ new Intl.DateTimeFormat("en-US", { timeZone: tz.trim() }).format();
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function isValidResetHour(hour: unknown): hour is number {
+ return typeof hour === "number" && Number.isInteger(hour) && hour >= 0 && hour <= 23;
+}
+
+export function nodeDailyResetConfigured(timezone: unknown, hour: unknown): boolean {
+ return typeof timezone === "string" && isValidIanaTimeZone(timezone) && isValidResetHour(hour);
+}
+
+type ZonedParts = {
+ year: number;
+ month: number;
+ day: number;
+ hour: number;
+ minute: number;
+ second: number;
+};
+
+function zonedParts(ms: number, timeZone: string): ZonedParts {
+ const fmt = new Intl.DateTimeFormat("en-US", {
+ timeZone,
+ hourCycle: "h23",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ });
+ const bag: Record = {};
+ for (const part of fmt.formatToParts(new Date(ms))) {
+ if (part.type !== "literal") bag[part.type] = part.value;
+ }
+ return {
+ year: Number(bag.year),
+ month: Number(bag.month),
+ day: Number(bag.day),
+ hour: Number(bag.hour),
+ minute: Number(bag.minute),
+ second: Number(bag.second),
+ };
+}
+
+function addCalendarDay(year: number, month: number, day: number): {
+ year: number;
+ month: number;
+ day: number;
+} {
+ const utc = Date.UTC(year, month - 1, day + 1);
+ const dt = new Date(utc);
+ return { year: dt.getUTCFullYear(), month: dt.getUTCMonth() + 1, day: dt.getUTCDate() };
+}
+
+/** Convert wall-clock time in `timeZone` to epoch ms. */
+function zonedLocalToUtc(
+ year: number,
+ month: number,
+ day: number,
+ hour: number,
+ minute: number,
+ second: number,
+ timeZone: string,
+): number {
+ const wanted = Date.UTC(year, month - 1, day, hour, minute, second);
+ let guess = wanted;
+ for (let i = 0; i < 4; i++) {
+ const p = zonedParts(guess, timeZone);
+ const asIfUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
+ const delta = asIfUtc - wanted;
+ if (delta === 0) return guess;
+ guess -= delta;
+ }
+ return guess;
+}
+
+/**
+ * Next local `hour:00:00` in `timezone` strictly after `nowMs`.
+ * If now lands exactly on that instant, return the following cycle.
+ */
+export function nextDailyResetAtMs(timezone: string, hour: number, nowMs: number): number {
+ const now = zonedParts(nowMs, timezone);
+ let date = { year: now.year, month: now.month, day: now.day };
+ let next = zonedLocalToUtc(date.year, date.month, date.day, hour, 0, 0, timezone);
+ if (next <= nowMs) {
+ date = addCalendarDay(date.year, date.month, date.day);
+ next = zonedLocalToUtc(date.year, date.month, date.day, hour, 0, 0, timezone);
+ }
+ return next;
+}
+
+export function parseTpdLimitFromText(text: string): number | null {
+ const m = /limit:\s*(\d+)/i.exec(text);
+ if (!m) return null;
+ const n = Number(m[1]);
+ return Number.isFinite(n) ? n : null;
+}
+
+export function isTpdRateLimit(errorText: string | null | undefined): boolean {
+ return String(errorText || "")
+ .toLowerCase()
+ .includes("tpd rate limit");
+}
+
+export type TpdCooldownOptions = {
+ timezone?: unknown;
+ hour?: unknown;
+ nowMs?: number;
+ headerResetAtMs?: number | null;
+};
+
+/**
+ * Cooldown for a TPD 429. Header reset wins; else the node clock.
+ * Both missing → null (caller uses short 429, does not guess midnight).
+ */
+export function resolveTpdCooldownMs(
+ errorText: string | null | undefined,
+ options: TpdCooldownOptions = {},
+): number | null {
+ if (!isTpdRateLimit(errorText)) return null;
+ const now = options.nowMs ?? Date.now();
+ if (typeof options.headerResetAtMs === "number" && options.headerResetAtMs > now) {
+ return options.headerResetAtMs - now;
+ }
+ if (typeof options.timezone === "string" && isValidResetHour(options.hour)) {
+ if (!nodeDailyResetConfigured(options.timezone, options.hour)) return null;
+ return nextDailyResetAtMs(options.timezone, options.hour, now) - now;
+ }
+ return null;
+}
diff --git a/open-sse/services/genericQuotaFetcher.ts b/open-sse/services/genericQuotaFetcher.ts
index 10d58d4b37..81419d5a6b 100644
--- a/open-sse/services/genericQuotaFetcher.ts
+++ b/open-sse/services/genericQuotaFetcher.ts
@@ -25,10 +25,53 @@ import {
type QuotaInfo,
} from "./quotaPreflight.ts";
+type UsageFetcher = (
+ connection: Parameters[0],
+ options?: { forceRefresh?: boolean }
+) => Promise;
+
+let usageFetcherOverride: UsageFetcher | null = null;
+
// 60s — matches Codex's TTL. Long enough to avoid hammering upstream usage
// endpoints on every routing decision, short enough that a near-exhausted
// account is skipped within one minute of crossing its threshold.
const CACHE_TTL_MS = 60_000;
+/** Drop unused force-refresh flags once inner provider caches (60s–5min) have expired. */
+const PENDING_FORCE_REFRESH_TTL_MS = CACHE_TTL_MS * 5;
+/** key → Date.now() when invalidate asked the next fetch to force-refresh. */
+const pendingForceRefresh = new Map();
+/** key → last convert-null / throw while force-refresh was pending. */
+const pendingForceRefreshMiss = new Map();
+
+/** Test-only: inject the usage dispatcher; pass null to restore. */
+export function __setGenericUsageFetcherForTests(fetcher: UsageFetcher | null): void {
+ usageFetcherOverride = fetcher;
+}
+
+/** Test-only: backdate a pending force-refresh so TTL expiry is unit-testable. */
+export function __agePendingForceRefreshForTests(
+ provider: string,
+ connectionId: string,
+ ageMs: number
+): void {
+ pendingForceRefresh.set(cacheKey(provider, connectionId), Date.now() - ageMs);
+}
+
+/** Test-only: backdate a convert-null miss so the 60s hammer-guard is unit-testable. */
+export function __agePendingForceRefreshMissForTests(
+ provider: string,
+ connectionId: string,
+ ageMs: number
+): void {
+ pendingForceRefreshMiss.set(cacheKey(provider, connectionId), Date.now() - ageMs);
+}
+
+/** Test-only: drop all wrapper/flag maps so tests cannot leak across ids. */
+export function __resetGenericQuotaFetcherForTests(): void {
+ cache.clear();
+ pendingForceRefresh.clear();
+ pendingForceRefreshMiss.clear();
+}
interface CacheEntry {
quota: QuotaInfo;
@@ -38,15 +81,72 @@ interface CacheEntry {
const cache = new Map();
function cacheKey(provider: string, connectionId: string): string {
- return `${provider}::${connectionId}`;
+ return `${provider.trim()}::${connectionId.trim()}`;
}
-// Auto-cleanup stale entries — same shape as codexQuotaFetcher.
+function dropExpiredPendingForceRefresh(key: string, now: number): boolean {
+ const stampedAt = pendingForceRefresh.get(key);
+ if (stampedAt === undefined) return true;
+ if (now - stampedAt > PENDING_FORCE_REFRESH_TTL_MS) {
+ pendingForceRefresh.delete(key);
+ pendingForceRefreshMiss.delete(key);
+ return true;
+ }
+ return false;
+}
+
+// Lazy expiry on read — same as the provider breaker. Name stays `is*` because
+// callers only need a boolean; the map is not a public API.
+function isPendingForceRefresh(key: string, now: number = Date.now()): boolean {
+ if (dropExpiredPendingForceRefresh(key, now)) return false;
+ return pendingForceRefresh.has(key);
+}
+
+function markPendingForceRefreshMiss(key: string): void {
+ if (isPendingForceRefresh(key)) pendingForceRefreshMiss.set(key, Date.now());
+}
+
+function cachedQuotaIfFresh(
+ key: string,
+ forceRefresh: boolean,
+ now: number
+): QuotaInfo | null {
+ if (forceRefresh) return null;
+ const cached = cache.get(key);
+ if (cached && now - cached.fetchedAt < CACHE_TTL_MS) return cached.quota;
+ return null;
+}
+
+function isForceRefreshMissCooling(
+ key: string,
+ forceRefresh: boolean,
+ now: number
+): boolean {
+ if (!forceRefresh) return false;
+ const missedAt = pendingForceRefreshMiss.get(key);
+ return missedAt !== undefined && now - missedAt < CACHE_TTL_MS;
+}
+
+/** True when a concurrent 429 re-stamped a still-live flag during fetchUsage. */
+function isConcurrentForceRefresh(key: string, refreshStamp: number | undefined): boolean {
+ const currentStamp = pendingForceRefresh.get(key);
+ if (currentStamp === refreshStamp) return false;
+ return (
+ currentStamp !== undefined &&
+ Date.now() - currentStamp <= PENDING_FORCE_REFRESH_TTL_MS
+ );
+}
+
+// 5min — same as Codex. Expiry is lazy on read (`isPendingForceRefresh`);
+// this timer only reaps keys nobody fetches after the 5min TTL.
const _cacheCleanup = setInterval(() => {
const now = Date.now();
for (const [key, entry] of cache) {
if (now - entry.fetchedAt > CACHE_TTL_MS * 5) cache.delete(key);
}
+ for (const key of pendingForceRefresh.keys()) {
+ dropExpiredPendingForceRefresh(key, now);
+ }
}, 5 * 60_000);
if (typeof _cacheCleanup === "object" && "unref" in _cacheCleanup) {
(_cacheCleanup as { unref?: () => void }).unref?.();
@@ -217,24 +317,47 @@ function normalizeQuotaWindows(
export const fetchGenericQuota: QuotaFetcher = async (connectionId, connection) => {
if (!connection) return null;
const conn = connection as ConnectionInputs;
- const provider = typeof conn.provider === "string" ? conn.provider : null;
+ const provider = typeof conn.provider === "string" ? conn.provider.trim() : "";
if (!provider) return null;
const key = cacheKey(provider, connectionId);
- const cached = cache.get(key);
- if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
- return cached.quota;
- }
+ const now = Date.now();
+ const forceRefresh = isPendingForceRefresh(key, now);
+ const hit = cachedQuotaIfFresh(key, forceRefresh, now);
+ if (hit) return hit;
+ // convert-null / throw keep the force-refresh flag (agy inner caches are
+ // still stale) but must not hammer those endpoints on every routing tick.
+ if (isForceRefreshMissCooling(key, forceRefresh, now)) return null;
+
+ // Capture before await: a 429 during fetchUsage re-stamps this; writing
+ // the pre-429 snapshot would wipe that flag and recache stale quota.
+ const refreshStamp = pendingForceRefresh.get(key);
let usage: unknown;
try {
- usage = await getUsageForProvider(conn as Parameters[0]);
+ const fetchUsage = usageFetcherOverride ?? getUsageForProvider;
+ usage = await fetchUsage(conn as Parameters[0], {
+ ...(forceRefresh ? { forceRefresh: true } : {}),
+ });
} catch {
+ markPendingForceRefreshMiss(key);
return null;
}
const quota = convertUsageToQuotaInfo(usage);
- if (!quota) return null;
+ if (!quota) {
+ markPendingForceRefreshMiss(key);
+ return null;
+ }
+
+ // Concurrent 429 re-stamped a still-live flag — do not recache the
+ // pre-429 snapshot. A vanished or expired stamp is not a 429.
+ if (isConcurrentForceRefresh(key, refreshStamp)) {
+ return quota;
+ }
+
+ pendingForceRefresh.delete(key);
+ pendingForceRefreshMiss.delete(key);
// Refresh the static window catalog so the dashboard can render the right
// modal inputs without waiting for the user to open the page.
@@ -250,7 +373,33 @@ export const fetchGenericQuota: QuotaFetcher = async (connectionId, connection)
* fresh data instead of a 60s stale window.
*/
export function invalidateGenericQuotaCache(provider: string, connectionId: string): void {
- cache.delete(cacheKey(provider, connectionId));
+ const key = cacheKey(provider, connectionId);
+ cache.delete(key);
+ // Next fetch must bypass provider-inner usage caches (agy retrieveUserQuota /
+ // weekly are 60s–5min). Without this, dropping the 60s wrapper recaches stale.
+ // TTL matches those inner caches: after 5min the flag is a no-op.
+ pendingForceRefresh.set(key, Date.now());
+ pendingForceRefreshMiss.delete(key);
+}
+
+/**
+ * Drop the generic quota cache after an upstream 429, matching Codex's
+ * `invalidateCodexQuotaCache` on 429. Probe-origin failures must not mutate
+ * routing caches (#9817).
+ */
+export function invalidateGenericQuotaCacheOnStatus(args: {
+ provider: string | null | undefined;
+ connectionId: string | null | undefined;
+ status: number;
+ isolateProbe?: boolean;
+}): boolean {
+ if (args.isolateProbe === true) return false; // undefined from callers that omit isolateProbe must still invalidate
+ if (args.status !== 429) return false;
+ const provider = typeof args.provider === "string" ? args.provider.trim() : "";
+ const connectionId = typeof args.connectionId === "string" ? args.connectionId.trim() : "";
+ if (!provider || !connectionId) return false;
+ invalidateGenericQuotaCache(provider, connectionId);
+ return true;
}
/**
diff --git a/open-sse/services/gigachatAuth.ts b/open-sse/services/gigachatAuth.ts
index 1696acc0e7..8b79d9cf63 100644
--- a/open-sse/services/gigachatAuth.ts
+++ b/open-sse/services/gigachatAuth.ts
@@ -15,6 +15,22 @@ type GigachatTokenOptions = {
const DEFAULT_GIGACHAT_AUTH_URL = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth";
const DEFAULT_GIGACHAT_SCOPE = "GIGACHAT_API_PERS";
const CACHE_SKEW_MS = 60_000;
+const TOKEN_CACHE_MAX = 100;
+const INFLIGHT_MAX = 50;
+
+function evictOldest(cache: Map): void {
+ if (cache.size >= TOKEN_CACHE_MAX) {
+ const first = cache.keys().next().value;
+ if (first) cache.delete(first);
+ }
+}
+
+function evictOldestInflight(cache: Map>): void {
+ if (cache.size >= INFLIGHT_MAX) {
+ const first = cache.keys().next().value;
+ if (first) cache.delete(first);
+ }
+}
const tokenCache = new Map();
const inflightRequests = new Map>();
@@ -23,10 +39,12 @@ function getCacheKey(credentials: string, authUrl: string, scope: string) {
return `${authUrl}::${scope}::${credentials}`;
}
-function isFreshToken(token: GigachatTokenResult | undefined) {
+function isFreshToken(token: GigachatTokenResult | undefined, key?: string) {
if (!token?.accessToken || !token?.expiresAt) return false;
const expiresAtMs = new Date(token.expiresAt).getTime();
- return Number.isFinite(expiresAtMs) && expiresAtMs - Date.now() > CACHE_SKEW_MS;
+ const fresh = Number.isFinite(expiresAtMs) && expiresAtMs - Date.now() > CACHE_SKEW_MS;
+ if (!fresh && key) tokenCache.delete(key);
+ return fresh;
}
function normalizeExpiry(rawExpiry: unknown) {
@@ -59,7 +77,7 @@ export async function getGigachatAccessToken(
const cacheKey = getCacheKey(credentials, authUrl, scope);
const cached = tokenCache.get(cacheKey);
- if (isFreshToken(cached)) {
+ if (isFreshToken(cached, cacheKey)) {
return cached;
}
@@ -100,10 +118,12 @@ export async function getGigachatAccessToken(
accessToken,
expiresAt: normalizeExpiry(data.exp ?? data.expires_at),
};
+ evictOldest(tokenCache);
tokenCache.set(cacheKey, token);
return token;
})();
+ evictOldestInflight(inflightRequests);
inflightRequests.set(cacheKey, requestPromise);
try {
return await requestPromise;
diff --git a/open-sse/services/modelFamilyFallback.ts b/open-sse/services/modelFamilyFallback.ts
index 5e80fe0d40..6226c1217b 100644
--- a/open-sse/services/modelFamilyFallback.ts
+++ b/open-sse/services/modelFamilyFallback.ts
@@ -78,8 +78,9 @@ const FAMILY_FALLBACK_TEMPLATES: Record = {
"gemini-2.5-pro": ["gemini-2.5-pro-preview-06-05", "gemini-2.5-pro-exp-03-25"],
"gemini-2.5-pro-preview-06-05": ["gemini-2.5-pro", "gemini-2.5-pro-exp-03-25"],
- // Claude Mythos family (Fable 5) — flagship falls to the next-best Opus
- // tiers before the cheaper Sonnet, matching the Opus family ordering.
+ // Claude Mythos family — prefer the previous Fable before falling to Opus
+ // tiers and then the cheaper Sonnet, matching the flagship ordering.
+ "claude-fable-5-1": ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"],
"claude-fable-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-sonnet-5"],
// Claude Opus family
diff --git a/open-sse/services/moonshotQuotaFetcher.ts b/open-sse/services/moonshotQuotaFetcher.ts
new file mode 100644
index 0000000000..df80fe11b3
--- /dev/null
+++ b/open-sse/services/moonshotQuotaFetcher.ts
@@ -0,0 +1,229 @@
+/**
+ * moonshotQuotaFetcher.ts — Moonshot Open Platform balance quota fetcher
+ *
+ * GET {origin}/v1/users/me/balance
+ * { code: 0, data: { available_balance, voucher_balance, cash_balance } }
+ *
+ * Origin comes from the connection baseUrl (api.moonshot.cn or api.moonshot.ai).
+ * Do not hardcode .ai as a fallback for .cn keys.
+ *
+ * Cache: 60s in-memory. Registration: registerMoonshotQuotaFetcher() at startup.
+ */
+
+import { toNumber } from "@/shared/utils/numeric";
+import { registerQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
+import { registerMonitorFetcher } from "./quotaMonitor.ts";
+import { throttleQuotaFetch } from "./quotaFetchThrottle.ts";
+import {
+ isMoonshotOpenPlatformConnection,
+ moonshotBalanceUrl,
+ resolveMoonshotOrigin,
+} from "./usage/moonshotOpenPlatform.ts";
+import type { UsageQuota } from "./usage/quota.ts";
+
+const CACHE_TTL_MS = 60_000;
+
+export interface MoonshotQuota extends QuotaInfo {
+ availableBalance: number;
+ voucherBalance: number;
+ cashBalance: number;
+ origin: string;
+ limitReached: boolean;
+}
+
+interface CacheEntry {
+ quota: MoonshotQuota;
+ fetchedAt: number;
+}
+
+const quotaCache = new Map();
+
+const _cacheCleanup = setInterval(() => {
+ const now = Date.now();
+ for (const [key, entry] of quotaCache) {
+ if (now - entry.fetchedAt > CACHE_TTL_MS * 5) {
+ quotaCache.delete(key);
+ }
+ }
+}, 5 * 60_000);
+
+if (typeof _cacheCleanup === "object" && "unref" in _cacheCleanup) {
+ (_cacheCleanup as { unref?: () => void }).unref?.();
+}
+
+function toRecord(value: unknown): Record {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : {};
+}
+
+function parseMoonshotQuotaResponse(data: unknown, origin: string): MoonshotQuota | null {
+ const obj = toRecord(data);
+ const code = obj.code;
+ if (code !== 0 && code !== undefined) return null;
+ const payload = toRecord(obj.data);
+ if (!("available_balance" in payload) && !("availableBalance" in payload)) return null;
+ const availableBalance = toNumber(payload.available_balance ?? payload.availableBalance, 0);
+ const voucherBalance = toNumber(payload.voucher_balance ?? payload.voucherBalance, 0);
+ const cashBalance = toNumber(payload.cash_balance ?? payload.cashBalance, 0);
+ const limitReached = availableBalance <= 0;
+ const percentUsed = limitReached ? 1 : 0;
+ return {
+ used: percentUsed * 100,
+ total: 100,
+ percentUsed,
+ resetAt: null,
+ availableBalance,
+ voucherBalance,
+ cashBalance,
+ origin,
+ limitReached,
+ windows: { balance: { percentUsed, resetAt: null } },
+ };
+}
+
+function connectionApiKey(connection?: Record): string | null {
+ const apiKey = connection?.apiKey;
+ return typeof apiKey === "string" && apiKey.trim().length > 0 ? apiKey : null;
+}
+
+export async function fetchMoonshotQuota(
+ connectionId: string,
+ connection?: Record
+): Promise {
+ const cached = quotaCache.get(connectionId);
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
+ return cached.quota;
+ }
+
+ const apiKey = connectionApiKey(connection);
+ if (!apiKey) return null;
+
+ const origin = resolveMoonshotOrigin({
+ provider: typeof connection?.provider === "string" ? connection.provider : undefined,
+ providerSpecificData: connection?.providerSpecificData,
+ });
+ if (!origin) return null;
+
+ const url = moonshotBalanceUrl(origin);
+ const authHeader = ["Bearer", apiKey].join(" ");
+
+ try {
+ await throttleQuotaFetch();
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: authHeader,
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ signal: AbortSignal.timeout(8_000),
+ });
+
+ if (response.status === 401 || response.status === 403) {
+ quotaCache.delete(connectionId);
+ return null;
+ }
+ if (!response.ok) return null;
+
+ const data = await response.json();
+ const quota = parseMoonshotQuotaResponse(data, origin);
+ if (!quota) return null;
+ quotaCache.set(connectionId, { quota, fetchedAt: Date.now() });
+ return quota;
+ } catch {
+ return null;
+ }
+}
+
+export function invalidateMoonshotQuotaCache(connectionId: string): void {
+ quotaCache.delete(connectionId);
+}
+
+export type MoonshotUsageConnection = {
+ id?: string;
+ provider?: string;
+ apiKey?: string;
+ providerSpecificData?: unknown;
+};
+
+export async function getMoonshotOpenPlatformUsage(
+ connection: MoonshotUsageConnection
+): Promise<{
+ plan?: string;
+ quotas?: Record;
+ message?: string;
+ limitReached?: boolean;
+}> {
+ const origin = resolveMoonshotOrigin(connection);
+ if (!origin) {
+ return { message: "Not a Moonshot Open Platform connection." };
+ }
+ const quota = (await fetchMoonshotQuota(connection.id || "moonshot", {
+ apiKey: connection.apiKey,
+ provider: connection.provider,
+ providerSpecificData: connection.providerSpecificData,
+ })) as MoonshotQuota | null;
+ if (!quota) {
+ return { message: "Moonshot API key not available. Add a key to view usage." };
+ }
+ const domestic = origin.includes("moonshot.cn");
+ return {
+ plan: domestic ? "Kimi 开放平台(国内)" : "Kimi Open Platform",
+ quotas: buildMoonshotBalanceQuotas(quota, domestic ? "CNY" : "USD"),
+ limitReached: quota.limitReached,
+ };
+}
+
+function balanceQuota(
+ remaining: number,
+ remainingPercentage: number,
+ currency: string
+): UsageQuota {
+ return {
+ used: 0,
+ total: 0,
+ remaining,
+ remainingPercentage,
+ resetAt: null,
+ unlimited: true,
+ currency,
+ };
+}
+
+function buildMoonshotBalanceQuotas(
+ quota: MoonshotQuota,
+ currency: string
+): Record {
+ return {
+ available: balanceQuota(quota.availableBalance, quota.limitReached ? 0 : 100, currency),
+ voucher: balanceQuota(quota.voucherBalance, 100, currency),
+ cash: balanceQuota(quota.cashBalance, 100, currency),
+ };
+}
+
+export function registerMoonshotQuotaFetcher(): void {
+ registerQuotaFetcher("moonshot", fetchMoonshotQuota);
+ registerQuotaFetcher("kimi", fetchMoonshotQuota);
+ registerMonitorFetcher("moonshot", fetchMoonshotQuota);
+ registerMonitorFetcher("kimi", fetchMoonshotQuota);
+}
+
+export function registerMoonshotFetchersForNodes(
+ nodes: Array<{ id?: string | null; prefix?: string | null; baseUrl?: string | null }>
+): void {
+ for (const node of nodes) {
+ const origin = resolveMoonshotOrigin({}, node.baseUrl);
+ if (!origin) continue;
+ if (typeof node.id === "string" && node.id) {
+ registerQuotaFetcher(node.id, fetchMoonshotQuota);
+ registerMonitorFetcher(node.id, fetchMoonshotQuota);
+ }
+ if (typeof node.prefix === "string" && node.prefix) {
+ registerQuotaFetcher(node.prefix, fetchMoonshotQuota);
+ registerMonitorFetcher(node.prefix, fetchMoonshotQuota);
+ }
+ }
+}
+
+export { isMoonshotOpenPlatformConnection };
diff --git a/open-sse/services/obscura.ts b/open-sse/services/obscura.ts
new file mode 100644
index 0000000000..23c4917214
--- /dev/null
+++ b/open-sse/services/obscura.ts
@@ -0,0 +1,163 @@
+/**
+ * obscura.ts — Shared Obscura browser engine (#12274).
+ *
+ * Obscura (https://github.com/h4ckf0r0day/obscura) is a lightweight Rust
+ * headless browser (~30MB resident) that speaks the Chrome DevTools Protocol.
+ * Playwright's `chromium.connectOverCDP` drives it like a real Chrome, so the
+ * browser pool and the cloudflare-playground executor can both use it without
+ * holding a 150-400MB Chromium process.
+ *
+ * Lifecycle: one Obscura `serve` process is spawned lazily on first use and
+ * shared for the server's lifetime. Callers receive a fresh CDP connection on
+ * demand; closing the connection does not stop the shared server. Set
+ * OBSCURA_CDP_ENDPOINT to point at an already-running Obscura instead of
+ * spawning one here (the process is then not owned by this module). The
+ * module is also disabled entirely when OMNIROUTE_BROWSER_POOL=off.
+ */
+
+import { spawn, type ChildProcess } from "node:child_process";
+import { createServer } from "node:net";
+
+export interface ObscuraConnection {
+ /** Playwright Browser connected over CDP to the shared Obscura server. */
+ browser: import("playwright").Browser;
+ /** The spawned `obscura serve` process, or null when an external endpoint is used. */
+ child: ChildProcess | null;
+}
+
+let shared: { child: ChildProcess | null; endpoint: string } | null = null;
+let starting: Promise<{ child: ChildProcess | null; endpoint: string } | null> | null = null;
+
+export function isObscuraUsable(): boolean {
+ const flag = process.env.OMNIROUTE_BROWSER_POOL;
+ if (flag === undefined) return true;
+ return flag !== "off" && flag !== "0" && flag !== "false";
+}
+
+function findFreePort(): Promise {
+ return new Promise((resolve, reject) => {
+ const srv = createServer();
+ srv.once("error", reject);
+ srv.listen(0, "127.0.0.1", () => {
+ const address = srv.address();
+ srv.close(() => {
+ if (address && typeof address === "object") resolve(address.port);
+ else reject(new Error("obscura: could not allocate a free port"));
+ });
+ });
+ });
+}
+
+async function obscuraBinaryPath(): Promise {
+ const bin = process.env.OBSCURA_BIN;
+ if (bin) return bin;
+ const { resolve } = await import("node:path");
+ const { existsSync, accessSync, constants } = await import("node:fs");
+ const dirs = (process.env.PATH || "").split(":");
+ for (const dir of dirs) {
+ const candidate = resolve(dir, "obscura");
+ try {
+ accessSync(candidate, constants.X_OK);
+ if (existsSync(candidate)) return candidate;
+ } catch {
+ /* not executable here — keep looking */
+ }
+ }
+ return null;
+}
+
+async function waitForCdpEndpoint(endpoint: string, timeoutMs: number): Promise {
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ try {
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), 1500);
+ // Probe /json/version, not the base URL: Obscura's HTTP server answers
+ // the CDP info route, while a bare GET to "/" never completes a response.
+ const probe = endpoint.replace(/^ws/, "http").replace(/\/$/, "") + "/json/version";
+ const res = await fetch(probe, { signal: controller.signal });
+ clearTimeout(timer);
+ if (res.ok) return true;
+ } catch {
+ /* not up yet */
+ }
+ await new Promise((r) => setTimeout(r, 250));
+ }
+ return false;
+}
+
+/** Ensure the shared Obscura server is up; returns its endpoint or null. */
+export async function ensureObscuraServer(): Promise<{
+ child: ChildProcess | null;
+ endpoint: string;
+} | null> {
+ if (!isObscuraUsable()) return null;
+ if (shared) return shared;
+ if (starting) return starting;
+ starting = (async () => {
+ const endpoint = process.env.OBSCURA_CDP_ENDPOINT;
+ if (endpoint) {
+ shared = { child: null, endpoint };
+ return shared;
+ }
+ const bin = await obscuraBinaryPath();
+ if (!bin) return null;
+ const port = Number(process.env.OBSCURA_PORT) || (await findFreePort());
+ const child = spawn(bin, ["serve", "--port", String(port), "--host", "127.0.0.1"], {
+ stdio: ["ignore", "ignore", "pipe"],
+ });
+ child.stderr?.on("data", () => {}); // obscura logs verbosely — swallow
+ const endpointForServer = `http://127.0.0.1:${port}`;
+ // A bad binary path (or a binary that cannot serve) must not hold the
+ // readiness wait for the full timeout: bail as soon as the child exits
+ // (or fails to spawn at all — 'exit' alone misses an ENOENT 'error').
+ const died = new Promise((resolve) => {
+ child.once("exit", () => resolve(true));
+ child.once("error", () => resolve(true));
+ });
+ const ready = await Promise.race([
+ waitForCdpEndpoint(endpointForServer, 30_000),
+ died.then(() => false as const),
+ ]);
+ if (ready !== true) {
+ child.kill("SIGKILL");
+ return null;
+ }
+ shared = { child, endpoint: endpointForServer };
+ return shared;
+ })();
+ try {
+ return await starting;
+ } finally {
+ starting = null;
+ }
+}
+
+/**
+ * Connect Playwright to the shared Obscura server. Returns null when Obscura
+ * is disabled, not installed, or the server could not start (callers fall
+ * back to their previous Chromium strategy).
+ */
+export async function connectObscuraBrowser(): Promise {
+ const server = await ensureObscuraServer();
+ if (!server) return null;
+ try {
+ const { chromium } = await import("playwright");
+ const browser = await chromium.connectOverCDP(server.endpoint);
+ return { browser, child: server.child };
+ } catch {
+ return null;
+ }
+}
+
+/** Caution: this terminates the shared `obscura serve` process (process-lifetime anyway). */
+export function killSharedObscuraServer(): void {
+ if (shared?.child) {
+ try {
+ shared.child.kill("SIGKILL");
+ } catch {
+ /* ignore */
+ }
+ }
+ shared = null;
+}
diff --git a/open-sse/services/providerCostData.ts b/open-sse/services/providerCostData.ts
index d9a4e0a8ab..8464635c92 100644
--- a/open-sse/services/providerCostData.ts
+++ b/open-sse/services/providerCostData.ts
@@ -1,4 +1,4 @@
-import type { TierAssignment } from "./tierTypes";
+import { getPricingForModel as getDefaultPricingForModel } from "@/shared/constants/pricing";
import type { TierConfig } from "./tierTypes";
export interface ModelPricing {
@@ -11,6 +11,7 @@ export interface ModelPricing {
export const KNOWN_MODEL_PRICING: Record = {
"gpt-4o": { inputCostPer1M: 2.5, outputCostPer1M: 10.0, isFree: false },
"gpt-4o-mini": { inputCostPer1M: 0.15, outputCostPer1M: 0.6, isFree: false },
+ "claude-fable-5-1": { inputCostPer1M: 10.0, outputCostPer1M: 50.0, isFree: false },
"claude-fable-5": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false },
"claude-opus-5": { inputCostPer1M: 5.0, outputCostPer1M: 25.0, isFree: false },
"claude-opus-4-8": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false },
@@ -37,14 +38,26 @@ export const KNOWN_MODEL_PRICING: Record = {
};
export function getModelPricing(provider: string, model: string): ModelPricing {
- const directKey = model.toLowerCase();
- if (KNOWN_MODEL_PRICING[directKey]) {
- return KNOWN_MODEL_PRICING[directKey];
- }
const providerKey = `${provider}/${model}`.toLowerCase();
if (KNOWN_MODEL_PRICING[providerKey]) {
return KNOWN_MODEL_PRICING[providerKey];
}
+ const providerPricing = getDefaultPricingForModel(provider, model);
+ if (providerPricing) {
+ const inputCostPer1M = Number(providerPricing.input);
+ const outputCostPer1M = Number(providerPricing.output);
+ if (Number.isFinite(inputCostPer1M) && Number.isFinite(outputCostPer1M)) {
+ return {
+ inputCostPer1M,
+ outputCostPer1M,
+ isFree: inputCostPer1M === 0 && outputCostPer1M === 0,
+ };
+ }
+ }
+ const directKey = model.toLowerCase();
+ if (KNOWN_MODEL_PRICING[directKey]) {
+ return KNOWN_MODEL_PRICING[directKey];
+ }
return { inputCostPer1M: 5.0, outputCostPer1M: 15.0, isFree: false };
}
diff --git a/open-sse/services/quotaPreflight.ts b/open-sse/services/quotaPreflight.ts
index a6c7d99aef..e22d8597b6 100644
--- a/open-sse/services/quotaPreflight.ts
+++ b/open-sse/services/quotaPreflight.ts
@@ -21,12 +21,22 @@
import { isCompatibleProviderConnectionId } from "@/shared/utils/compatibleProviderId";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
import { fetchNewApiAggregatorQuota } from "./newApiAggregatorQuotaFetcher.ts";
+import {
+ isAntigravityQuotaProvider,
+ selectAntigravityQuotaWindowNames,
+} from "./antigravityQuotaFamily.ts";
export interface PreflightQuotaResult {
proceed: boolean;
reason?: string;
quotaPercent?: number;
resetAt?: string | null;
+ windowName?: string | null;
+}
+
+export interface QuotaCutoffScope {
+ provider?: string | null;
+ requestedModel?: string | null;
}
export interface QuotaWindowInfo {
@@ -156,15 +166,36 @@ function isRemainingAtOrBelowThreshold(
return remainingPercent <= thresholdPercent + REMAINING_PERCENT_EPSILON;
}
-function exhaustedResult(quotaPercent: number, resetAt: string | null): PreflightQuotaResult {
+function exhaustedResult(
+ quotaPercent: number,
+ resetAt: string | null,
+ windowName?: string | null
+): PreflightQuotaResult {
return {
proceed: false,
reason: "quota_exhausted",
quotaPercent,
resetAt,
+ windowName: windowName ?? null,
};
}
+function windowsForScope(
+ windows: NonNullable,
+ scope?: QuotaCutoffScope
+): NonNullable {
+ if (!scope?.requestedModel || !isAntigravityQuotaProvider(scope.provider ?? null)) {
+ return windows;
+ }
+ const selected = selectAntigravityQuotaWindowNames(Object.keys(windows), scope.requestedModel);
+ if (selected.length === 0) return windows;
+ const scoped: NonNullable = {};
+ for (const name of selected) {
+ if (windows[name]) scoped[name] = windows[name];
+ }
+ return Object.keys(scoped).length > 0 ? scoped : windows;
+}
+
function limitReachedResult(quota: QuotaInfo): PreflightQuotaResult {
return exhaustedResult(
Number.isFinite(quota.percentUsed) ? quota.percentUsed : 1,
@@ -201,7 +232,9 @@ function quotaWindowCutoffResult(
worstResetAt = windowInfo.resetAt ?? null;
}
- return worstWindow === null ? null : exhaustedResult(worstUsedPercent, worstResetAt);
+ return worstWindow === null
+ ? null
+ : exhaustedResult(worstUsedPercent, worstResetAt, worstWindow);
}
function quotaPercentCutoffResult(
@@ -227,21 +260,27 @@ function quotaPercentCutoffResult(
*/
export function evaluateQuotaCutoff(
quota: QuotaInfo | null | undefined,
- thresholds?: PreflightQuotaThresholds
+ thresholds?: PreflightQuotaThresholds,
+ scope?: QuotaCutoffScope
): PreflightQuotaResult {
if (!quota) return { proceed: true };
- if (quota.limitReached === true) return limitReachedResult(quota);
const windows = quota.windows;
if (windows && Object.keys(windows).length > 0) {
- return (
- quotaWindowCutoffResult(windows, thresholds) ?? {
- proceed: true,
- quotaPercent: quota.percentUsed,
- }
- );
+ const scopedWindows = windowsForScope(windows, scope);
+ const cutoff = quotaWindowCutoffResult(scopedWindows, thresholds);
+ if (cutoff) return cutoff;
+ if (isAntigravityQuotaProvider(scope?.provider ?? null) && scope?.requestedModel) {
+ return { proceed: true, quotaPercent: quota.percentUsed };
+ }
+ if (quota.limitReached === true) return limitReachedResult(quota);
+ return {
+ proceed: true,
+ quotaPercent: quota.percentUsed,
+ };
}
+ if (quota.limitReached === true) return limitReachedResult(quota);
return quotaPercentCutoffResult(quota, thresholds);
}
@@ -297,61 +336,40 @@ export async function preflightQuota(
return { proceed: true };
}
- if (quota.limitReached === true) {
- return limitReachedResult(quota);
- }
-
- // Per-window evaluation — only when the fetcher surfaces a windows map.
- // We block as soon as ANY single window's remaining quota drops to its
- // configured cutoff or below; warnings are logged independently per window.
- if (quota.windows && Object.keys(quota.windows).length > 0) {
- let worstUsedPercent = 0;
- let worstWindow: string | null = null;
- let worstResetAt: string | null = null;
- for (const [windowName, windowInfo] of Object.entries(quota.windows)) {
- const minRemainingPercent = resolveOrDefault(
- thresholds?.resolveMinRemainingPercent,
- windowName,
- DEFAULT_MIN_REMAINING_PERCENT
- );
+ const requestedModel =
+ typeof connection.requestedModel === "string" ? connection.requestedModel : null;
+ const scope: QuotaCutoffScope = { provider, requestedModel };
+ const windows = quota.windows;
+ if (windows && Object.keys(windows).length > 0) {
+ const scopedWindows = windowsForScope(windows, scope);
+ for (const [windowName, windowInfo] of Object.entries(scopedWindows)) {
const warnRemainingPercent = resolveOrDefault(
thresholds?.resolveWarnRemainingPercent,
windowName,
DEFAULT_WARN_REMAINING_PERCENT
);
const remainingPercent = remainingPercentFrom(windowInfo.percentUsed);
-
- if (isRemainingAtOrBelowThreshold(remainingPercent, minRemainingPercent)) {
- // Track the most-depleted blocking window so the response can name it.
- if (windowInfo.percentUsed > worstUsedPercent) {
- worstUsedPercent = windowInfo.percentUsed;
- worstWindow = windowName;
- worstResetAt = windowInfo.resetAt ?? null;
- } else if (worstWindow === null) {
- worstWindow = windowName;
- worstResetAt = windowInfo.resetAt ?? null;
- }
- } else if (isRemainingAtOrBelowThreshold(remainingPercent, warnRemainingPercent)) {
+ if (isRemainingAtOrBelowThreshold(remainingPercent, warnRemainingPercent)) {
console.warn(
`[QuotaPreflight] ${provider}/${connectionId} ${windowName}: ${remainingPercent.toFixed(1)}% remaining — approaching cutoff`
);
}
}
+ }
- if (worstWindow !== null) {
- const worstRemaining = remainingPercentFrom(worstUsedPercent);
- console.info(
- `[QuotaPreflight] ${provider}/${connectionId} ${worstWindow}: ${worstRemaining.toFixed(1)}% remaining — switching`
- );
- return {
- proceed: false,
- reason: "quota_exhausted",
- quotaPercent: worstUsedPercent,
- resetAt: worstResetAt,
- };
- }
-
- return { proceed: true, quotaPercent: quota.percentUsed };
+ const decision = evaluateQuotaCutoff(quota, thresholds, scope);
+ if (!decision.proceed) {
+ const windowLabel = decision.windowName ? ` ${decision.windowName}` : "";
+ const remaining = Number.isFinite(decision.quotaPercent)
+ ? remainingPercentFrom(decision.quotaPercent as number).toFixed(1)
+ : "?";
+ console.info(
+ `[QuotaPreflight] ${provider}/${connectionId}${windowLabel}: ${remaining}% remaining - switching`
+ );
+ return decision;
+ }
+ if (windows && Object.keys(windows).length > 0) {
+ return decision;
}
// Legacy single-signal path for fetchers that don't expose per-window data.
diff --git a/open-sse/services/responsesInputSanitizer.ts b/open-sse/services/responsesInputSanitizer.ts
index 94cd99f934..48f1eed051 100644
--- a/open-sse/services/responsesInputSanitizer.ts
+++ b/open-sse/services/responsesInputSanitizer.ts
@@ -11,6 +11,10 @@ const SERVER_ITEM_ID_PREFIX_BY_TYPE: Record = {
reasoning: "rs_",
};
const SERVER_ITEM_ID_PATTERN = /^(fc|msg|rs|resp)_/;
+// Validated per input item of type function_call / function_call_output (the agentic
+// Responses path), so kept as a module constant instead of an inline literal.
+const FUNCTION_NAME_VALID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
+const FUNCTION_NAME_SANITIZE_RE = /[^a-zA-Z0-9_-]/g;
function toRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
@@ -38,7 +42,7 @@ export function isInternalAssistantMessage(record: JsonRecord): boolean {
// Sanitize after cloning so upstream never sees an invalid name.
function sanitizeFunctionName(name: string): string {
// Replace any character not in [a-zA-Z0-9_-] with underscore, then truncate.
- return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
+ return name.replace(FUNCTION_NAME_SANITIZE_RE, "_").slice(0, 128);
}
function sanitizeInputItemId(record: JsonRecord): JsonRecord {
@@ -149,7 +153,7 @@ function sanitizeInputItem(item: unknown): unknown {
if (
(next.type === "function_call" || next.type === "function_call_output") &&
typeof next.name === "string" &&
- !/^[a-zA-Z0-9_-]{1,128}$/.test(next.name)
+ !FUNCTION_NAME_VALID_RE.test(next.name)
) {
next = { ...next, name: sanitizeFunctionName(next.name) };
}
diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts
index ea240340ea..62e96ce1bc 100644
--- a/open-sse/services/usage.ts
+++ b/open-sse/services/usage.ts
@@ -61,6 +61,8 @@ import { getQoderUsage, parseQoderUserStatusUsage } from "./usage/qoder.ts";
export { parseQoderUserStatusUsage } from "./usage/qoder.ts";
import { getOpencodeUsage } from "./usage/opencode.ts";
import { getDeepseekUsage } from "./usage/deepseek.ts";
+import { getMoonshotOpenPlatformUsage } from "./moonshotQuotaFetcher.ts";
+import { isMoonshotOpenPlatformConnection } from "./usage/moonshotOpenPlatform.ts";
import { getDevinCliUsage } from "./usage/devinCli.ts";
import { getBailianCodingPlanUsage } from "./usage/bailian.ts";
import { getVertexUsage } from "./usage/vertex.ts";
@@ -111,6 +113,10 @@ export async function getUsageForProvider(
) {
const { id, provider, accessToken, apiKey, providerSpecificData, projectId, email } = connection;
+ if (isMoonshotOpenPlatformConnection(connection)) {
+ return await getMoonshotOpenPlatformUsage(connection);
+ }
+
switch (provider) {
case "github":
return await getGitHubUsage(accessToken, providerSpecificData);
@@ -168,6 +174,9 @@ export async function getUsageForProvider(
return await getNanoGptUsage(apiKey || "");
case "deepseek":
return await getDeepseekUsage(id || "", apiKey || "");
+ case "moonshot":
+ case "kimi":
+ return await getMoonshotOpenPlatformUsage(connection);
case "openrouter":
return await getOpenrouterUsage(id || "", apiKey || "", providerSpecificData);
case "opencode":
diff --git a/open-sse/services/usage/fetcherProviders.ts b/open-sse/services/usage/fetcherProviders.ts
index bfcc5da710..05a3b47225 100644
--- a/open-sse/services/usage/fetcherProviders.ts
+++ b/open-sse/services/usage/fetcherProviders.ts
@@ -45,6 +45,8 @@ export const USAGE_FETCHER_PROVIDERS = [
"qwen-cloud-token-plan",
"nanogpt",
"deepseek",
+ "moonshot",
+ "kimi",
"opencode",
"opencode-zen",
"xiaomi-mimo",
diff --git a/open-sse/services/usage/grokCli.ts b/open-sse/services/usage/grokCli.ts
index 08396cfb2f..bbd76199b8 100644
--- a/open-sse/services/usage/grokCli.ts
+++ b/open-sse/services/usage/grokCli.ts
@@ -239,9 +239,12 @@ export async function getGrokCliUsage(accessToken?: string) {
const config = billing.config;
const resetAt = config.currentPeriod?.end || null;
const quotas: Record> = {};
- if (config.creditUsagePercent != null) {
- quotas.weekly = percentageQuota(config.creditUsagePercent, resetAt);
- }
+ // SuperGrokPro (and proto3 omit-zero) billing configs often omit
+ // creditUsagePercent / productUsage. A present config object is a
+ // successful billing read, so treat a missing percent as 0% used and
+ // still render a weekly bar. A missing config still returns
+ // "Grok Build billing status unavailable" above — that path is unchanged.
+ quotas.weekly = percentageQuota(config.creditUsagePercent ?? 0, resetAt);
Object.assign(quotas, buildProductQuotas(config.productUsage, resetAt));
const autoTopUpResponse = userId
diff --git a/open-sse/services/usage/moonshotOpenPlatform.ts b/open-sse/services/usage/moonshotOpenPlatform.ts
new file mode 100644
index 0000000000..a652aacb36
--- /dev/null
+++ b/open-sse/services/usage/moonshotOpenPlatform.ts
@@ -0,0 +1,76 @@
+/**
+ * Moonshot Open Platform host recognition.
+ *
+ * Distinguishes prepaid Open Platform keys (api.moonshot.cn / api.moonshot.ai)
+ * from Kimi Coding Plan (api.kimi.com/coding). Custom compatible nodes are
+ * identified by baseUrl host, not by provider id (those ids are uuids).
+ */
+
+import { moonshotProvider } from "../../config/providers/registry/moonshot/index.ts";
+import { kimiProvider } from "../../config/providers/registry/kimi/index.ts";
+
+export const MOONSHOT_OPEN_PLATFORM_HOSTS: ReadonlySet = new Set([
+ "api.moonshot.cn",
+ "api.moonshot.ai",
+]);
+
+export type MoonshotOriginConnection = {
+ provider?: string;
+ providerSpecificData?: unknown;
+};
+
+function asRecord(value: unknown): Record {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : {};
+}
+
+export function parseMoonshotOrigin(baseUrl: string | null | undefined): string | null {
+ if (typeof baseUrl !== "string" || baseUrl.trim() === "") return null;
+ let url: URL;
+ try {
+ url = new URL(baseUrl.trim());
+ } catch {
+ return null;
+ }
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
+ const host = url.hostname.toLowerCase();
+ if (!MOONSHOT_OPEN_PLATFORM_HOSTS.has(host)) return null;
+ const port = url.port ? `:${url.port}` : "";
+ return `${url.protocol}//${host}${port}`;
+}
+
+export function moonshotBalanceUrl(origin: string): string {
+ return `${origin}/v1/users/me/balance`;
+}
+
+function registryDefaultOrigin(provider: string | undefined): string | null {
+ if (provider === "moonshot") return parseMoonshotOrigin(moonshotProvider.baseUrl);
+ if (provider === "kimi") return parseMoonshotOrigin(kimiProvider.baseUrl);
+ return null;
+}
+
+export function resolveMoonshotOrigin(
+ connection: MoonshotOriginConnection,
+ nodeBaseUrl?: string | null,
+): string | null {
+ const psd = asRecord(connection.providerSpecificData);
+ const fromPsd = typeof psd.baseUrl === "string" ? parseMoonshotOrigin(psd.baseUrl) : null;
+ if (fromPsd) return fromPsd;
+ const fromNode = parseMoonshotOrigin(nodeBaseUrl);
+ if (fromNode) return fromNode;
+ return registryDefaultOrigin(connection.provider);
+}
+
+export function isMoonshotOpenPlatformConnection(
+ connection: MoonshotOriginConnection,
+ nodeBaseUrl?: string | null,
+): boolean {
+ return resolveMoonshotOrigin(connection, nodeBaseUrl) !== null;
+}
+
+/** Account-level empty wallet on Open Platform. Narrower than any compatible 429. */
+export function isMoonshotAccountBalanceExhausted(errorText: string | null | undefined): boolean {
+ const lower = String(errorText || "").toLowerCase();
+ return lower.includes("insufficient balance") || lower.includes("exceeded_current_quota");
+}
diff --git a/open-sse/services/usage/supportedProviders.ts b/open-sse/services/usage/supportedProviders.ts
index dc088fa1c4..b8e0481b86 100644
--- a/open-sse/services/usage/supportedProviders.ts
+++ b/open-sse/services/usage/supportedProviders.ts
@@ -41,6 +41,8 @@ export const USAGE_SUPPORTED_PROVIDERS: readonly string[] = [
"crof",
"nanogpt",
"deepseek",
+ "moonshot",
+ "kimi",
"xiaomi-mimo",
"xiaomi-mimo-token-plan",
"vertex",
diff --git a/open-sse/translator/bootstrap.ts b/open-sse/translator/bootstrap.ts
index df852d483c..bd870e934a 100644
--- a/open-sse/translator/bootstrap.ts
+++ b/open-sse/translator/bootstrap.ts
@@ -5,6 +5,7 @@
import "./request/claude-to-openai.ts";
import "./request/openai-to-claude.ts";
+import "./request/openai-to-clova.ts";
import "./request/gemini-to-openai.ts";
import "./request/openai-to-gemini.ts";
import "./request/antigravity-to-openai.ts";
@@ -15,6 +16,7 @@ import "./request/claude-to-gemini.ts";
import "./response/claude-to-openai.ts";
import "./response/openai-to-claude.ts";
+import "./response/clova-to-openai.ts";
import "./response/gemini-to-openai.ts";
import "./response/gemini-to-claude.ts";
import "./response/openai-to-antigravity.ts";
diff --git a/open-sse/translator/formats.ts b/open-sse/translator/formats.ts
index 4e0bd391f0..1349d30963 100644
--- a/open-sse/translator/formats.ts
+++ b/open-sse/translator/formats.ts
@@ -5,6 +5,8 @@ export const FORMATS = {
OPENAI_RESPONSE: "openai-response",
CLAUDE: "claude",
GEMINI: "gemini",
+ /** Naver CLOVA Studio Chat Completions v3 (native envelope, model in URL path). */
+ CLOVA: "clova",
CODEX: "codex",
ANTIGRAVITY: "antigravity",
KIRO: "kiro",
diff --git a/open-sse/translator/request/openai-to-clova.ts b/open-sse/translator/request/openai-to-clova.ts
new file mode 100644
index 0000000000..62bd519c66
--- /dev/null
+++ b/open-sse/translator/request/openai-to-clova.ts
@@ -0,0 +1,375 @@
+/**
+ * OpenAI → Naver CLOVA Studio "Chat Completions v3" request translator.
+ *
+ * Wire format: `POST https://clovastudio.stream.ntruss.com/v3/chat-completions/{modelName}`
+ *
+ * Everything below that is marked "live-verified" was confirmed against the real
+ * API on 2026-09-01 — several of these rules contradict a plausible reading of
+ * the vendor docs, so they are recorded with the evidence.
+ *
+ * Vendor docs:
+ * - text/image: https://api.ncloud-docs.com/docs/clovastudio-chatcompletionsv3
+ * - thinking: https://api.ncloud-docs.com/docs/clovastudio-chatcompletionsv3-thinking
+ * - FC: https://api.ncloud-docs.com/docs/clovastudio-chatcompletionsv3-fc
+ * - SO: https://api.ncloud-docs.com/docs/clovastudio-chatcompletionsv3-so
+ */
+import { register } from "../registry.ts";
+import { FORMATS } from "../formats.ts";
+
+/** Output cap for the non-reasoning v3 models (HCX-005, HCX-DASH-002). */
+export const CLOVA_V3_MAX_OUTPUT_TOKENS = 4096;
+
+/** Output cap for the reasoning model (HCX-007) — includes thinking tokens. */
+export const CLOVA_V3_REASONING_MAX_OUTPUT_TOKENS = 32768;
+
+/**
+ * Function calling rejects any cap below 1024 (live-verified: `40001 Invalid
+ * parameter: tools, maxTokens`).
+ */
+export const CLOVA_V3_MIN_TOOL_TOKENS = 1024;
+
+export const CLOVA_V3_REASONING_MODELS: ReadonlySet = new Set(["HCX-007"]);
+
+export const CLOVA_V3_VISION_MODELS: ReadonlySet = new Set(["HCX-005"]);
+
+/**
+ * All three v3 models accept function calling (live-verified). HCX-007 needs
+ * `thinking.effort: "none"` alongside it or the call fails with
+ * `40001 Invalid parameter: tools, thinking`.
+ */
+export const CLOVA_V3_FUNCTION_CALLING_MODELS: ReadonlySet = new Set([
+ "HCX-005",
+ "HCX-007",
+ "HCX-DASH-002",
+]);
+
+/** Structured Outputs is HCX-007 only (live-verified: HCX-005 rejects `thinking`). */
+export const CLOVA_V3_STRUCTURED_OUTPUT_MODELS: ReadonlySet = new Set(["HCX-007"]);
+
+const CLOVA_THINKING_EFFORTS: ReadonlySet = new Set(["none", "low", "medium", "high"]);
+
+type JsonRecord = Record;
+
+function toRecord(value: unknown): JsonRecord | null {
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
+}
+
+function nonEmptyString(value: unknown): string {
+ return typeof value === "string" && value.length > 0 ? value : "";
+}
+
+export function isClovaReasoningModel(model: string): boolean {
+ return typeof model === "string" && CLOVA_V3_REASONING_MODELS.has(model.toUpperCase());
+}
+
+export function isClovaVisionModel(model: string): boolean {
+ return typeof model === "string" && CLOVA_V3_VISION_MODELS.has(model.toUpperCase());
+}
+
+export function isClovaFunctionCallingModel(model: string): boolean {
+ return typeof model === "string" && CLOVA_V3_FUNCTION_CALLING_MODELS.has(model.toUpperCase());
+}
+
+export function isClovaStructuredOutputModel(model: string): boolean {
+ return typeof model === "string" && CLOVA_V3_STRUCTURED_OUTPUT_MODELS.has(model.toUpperCase());
+}
+
+function clampNumeric(value: unknown, min: number, max: number): number | null {
+ const n = typeof value === "string" ? Number(value) : value;
+ if (typeof n !== "number" || !Number.isFinite(n)) return null;
+ return Math.min(Math.max(n, min), max);
+}
+
+/**
+ * Map OpenAI `reasoning_effort` onto CLOVA's `thinking.effort`.
+ * `minimal` collapses to `low`; unrecognised values are dropped so CLOVA applies
+ * its own default (`low`).
+ */
+export function toClovaThinkingEffort(reasoningEffort: unknown): string {
+ if (typeof reasoningEffort !== "string") return "";
+ const effort = reasoningEffort.toLowerCase();
+ if (effort === "minimal") return "low";
+ return CLOVA_THINKING_EFFORTS.has(effort) ? effort : "";
+}
+
+/** Flatten OpenAI message content into a single string (text only). */
+function contentToString(content: unknown): string {
+ if (typeof content === "string") return content;
+ if (!Array.isArray(content)) return content == null ? "" : String(content);
+ return content
+ .map((part) =>
+ part && typeof part === "object" && typeof part.text === "string" ? part.text : ""
+ )
+ .filter(Boolean)
+ .join("\n");
+}
+
+/**
+ * Convert an OpenAI `content` value into CLOVA v3 typed content parts.
+ *
+ * Both image transports work (live-verified): a public URL becomes
+ * `imageUrl.url`, and a `data:` URL becomes `dataUri.data` — which must keep the
+ * FULL `data:;base64,` prefix or CLOVA rejects the request with
+ * `40001 Invalid parameter`.
+ */
+export function toClovaContent(
+ content: unknown,
+ supportsImages: boolean
+): Array> {
+ if (typeof content === "string") {
+ return [{ type: "text", text: content }];
+ }
+
+ if (!Array.isArray(content)) {
+ return [{ type: "text", text: content == null ? "" : String(content) }];
+ }
+
+ const parts = content
+ .map((part) => toClovaContentPart(part, supportsImages))
+ .filter((part): part is JsonRecord => part !== null);
+
+ // CLOVA rejects a message with an empty content array, so always emit a part.
+ return parts.length > 0 ? parts : [{ type: "text", text: "" }];
+}
+
+function toClovaContentPart(part: unknown, supportsImages: boolean): JsonRecord | null {
+ const record = toRecord(part);
+ if (!record) return null;
+
+ const text = nonEmptyString(record.text);
+ if (record.type === "text" || text) return text ? { type: "text", text } : null;
+ if (record.type !== "image_url" || !supportsImages) return null;
+
+ const imageUrl = toRecord(record.image_url);
+ const url = nonEmptyString(imageUrl?.url) || nonEmptyString(record.url);
+ if (!url) return null;
+ return url.startsWith("data:")
+ ? { type: "image_url", dataUri: { data: url } }
+ : { type: "image_url", imageUrl: { url } };
+}
+
+/** Parse OpenAI's JSON-string tool arguments into the object CLOVA expects. */
+function toolArgumentsToObject(raw: unknown): Record {
+ if (raw == null) return {};
+ if (typeof raw === "object") return raw as Record;
+ if (typeof raw !== "string" || !raw.trim()) return {};
+ try {
+ const parsed = JSON.parse(raw);
+ return parsed && typeof parsed === "object" ? (parsed as Record) : {};
+ } catch {
+ return {};
+ }
+}
+
+/**
+ * Convert OpenAI tool declarations into CLOVA's `tools` array.
+ * The shapes are nearly identical; empty declarations are skipped because CLOVA
+ * rejects a tool without a name.
+ */
+export function toClovaTools(tools: unknown): Array> {
+ if (!Array.isArray(tools)) return [];
+ return tools.map(toClovaTool).filter((tool): tool is JsonRecord => tool !== null);
+}
+
+function toClovaTool(tool: unknown): JsonRecord | null {
+ const record = toRecord(tool);
+ if (!record) return null;
+ const fn = toRecord(record.function);
+ const name = nonEmptyString(fn?.name) || nonEmptyString(record.name);
+ if (!name) return null;
+
+ const description =
+ nonEmptyString(fn?.description) || nonEmptyString(record.description) || `Tool: ${name}`;
+ const parameters = fn?.parameters ?? record.parameters;
+ return {
+ type: "function",
+ function: {
+ name,
+ description,
+ ...(parameters ? { parameters } : {}),
+ },
+ };
+}
+
+/**
+ * Which mutually-exclusive v3 mode does this request use?
+ *
+ * CLOVA forbids combining function calling with thinking or images, and forbids
+ * combining structured outputs with either. Exactly one mode is chosen.
+ */
+export function resolveClovaMode(
+ model: string,
+ body: Record
+): "tools" | "structured" | "plain" {
+ const tools = toClovaTools(body?.tools);
+ if (tools.length > 0 && isClovaFunctionCallingModel(model)) return "tools";
+
+ const format = toRecord(body?.response_format);
+ const wantsSchema = format && (format.type === "json_schema" || format.type === "json_object");
+ if (wantsSchema && isClovaStructuredOutputModel(model)) return "structured";
+
+ return "plain";
+}
+
+type ClovaMode = "tools" | "structured" | "plain";
+
+function normalizeMessageRole(role: unknown): "assistant" | "system" | "user" {
+ return role === "assistant" || role === "system" ? role : "user";
+}
+
+function toClovaToolCall(call: unknown): JsonRecord {
+ const record = toRecord(call) ?? {};
+ const fn = toRecord(record.function);
+ return {
+ id: record.id ?? "",
+ type: "function",
+ function: {
+ name: fn?.name ?? record.name ?? "",
+ arguments: toolArgumentsToObject(fn?.arguments ?? record.arguments),
+ },
+ };
+}
+
+function toClovaToolModeMessage(message: unknown): JsonRecord {
+ const record = toRecord(message) ?? {};
+ if (record.role === "tool") {
+ return {
+ role: "tool",
+ content: contentToString(record.content),
+ ...(record.tool_call_id ? { toolCallId: String(record.tool_call_id) } : {}),
+ };
+ }
+
+ const toolCalls = Array.isArray(record.tool_calls) ? record.tool_calls : [];
+ if (record.role === "assistant" && toolCalls.length > 0) {
+ return {
+ role: "assistant",
+ content: "",
+ toolCalls: toolCalls.map(toClovaToolCall),
+ };
+ }
+ return {
+ role: normalizeMessageRole(record.role),
+ content: contentToString(record.content),
+ };
+}
+
+function toClovaPlainMessage(message: unknown, supportsImages: boolean): JsonRecord {
+ const record = toRecord(message) ?? {};
+ return {
+ role: normalizeMessageRole(record.role),
+ content: toClovaContent(record.content, supportsImages),
+ };
+}
+
+function toClovaMessages(body: JsonRecord, mode: ClovaMode, supportsImages: boolean): JsonRecord[] {
+ const messages = Array.isArray(body.messages) ? body.messages : [];
+ return messages.map((message) =>
+ mode === "tools"
+ ? toClovaToolModeMessage(message)
+ : toClovaPlainMessage(message, supportsImages)
+ );
+}
+
+function applyThinking(payload: JsonRecord, body: JsonRecord, reasoning: boolean, mode: ClovaMode) {
+ if (!reasoning) return;
+ const effort = toClovaThinkingEffort(body.reasoning_effort);
+ if (mode === "tools" || mode === "structured") {
+ payload.thinking = { effort: "none" };
+ } else if (effort) {
+ payload.thinking = { effort };
+ }
+}
+
+function applySampling(payload: JsonRecord, body: JsonRecord): void {
+ const temperature = clampNumeric(body.temperature, 0, 1);
+ if (temperature !== null) payload.temperature = temperature;
+ const topP = clampNumeric(body.top_p, 0, 1);
+ if (topP !== null && topP > 0) payload.topP = topP;
+ const topK = clampNumeric(body.top_k, 0, 128);
+ if (topK !== null && topK > 0) payload.topK = topK;
+ const penalty = clampNumeric(body.repetition_penalty, 0, 2);
+ if (penalty !== null && penalty > 0) payload.repetitionPenalty = penalty;
+}
+
+function applyOutputCap(
+ payload: JsonRecord,
+ body: JsonRecord,
+ reasoning: boolean,
+ mode: ClovaMode
+): void {
+ const cap = reasoning ? CLOVA_V3_REASONING_MAX_OUTPUT_TOKENS : CLOVA_V3_MAX_OUTPUT_TOKENS;
+ const key = reasoning ? "maxCompletionTokens" : "maxTokens";
+ let tokens = clampNumeric(body.max_completion_tokens ?? body.max_tokens, 1, cap);
+ if (mode === "tools") {
+ const floor = Math.min(CLOVA_V3_MIN_TOOL_TOKENS, cap);
+ tokens = tokens === null ? floor : Math.max(tokens, floor);
+ }
+ if (tokens !== null) payload[key] = tokens;
+}
+
+function responseSchema(body: JsonRecord): unknown {
+ const format = toRecord(body.response_format);
+ const jsonSchema = toRecord(format?.json_schema);
+ return jsonSchema?.schema ?? format?.schema;
+}
+
+function applyModeFields(payload: JsonRecord, body: JsonRecord, mode: ClovaMode): void {
+ if (mode === "tools") {
+ payload.tools = toClovaTools(body.tools);
+ if (body.tool_choice === "none") payload.toolChoice = "none";
+ if (body.tool_choice === "auto" || body.tool_choice === "required") {
+ payload.toolChoice = "auto";
+ }
+ return;
+ }
+ if (mode !== "structured") return;
+ const schema = responseSchema(body);
+ if (schema && typeof schema === "object") {
+ payload.responseFormat = { type: "json", schema };
+ } else {
+ delete payload.thinking;
+ }
+}
+
+function applyPlainOptions(
+ payload: JsonRecord,
+ body: JsonRecord,
+ reasoning: boolean,
+ mode: ClovaMode
+): void {
+ if (mode === "plain" && !reasoning) {
+ if (Array.isArray(body.stop) && body.stop.length > 0) {
+ payload.stop = body.stop.filter((value) => typeof value === "string");
+ } else if (typeof body.stop === "string" && body.stop) {
+ payload.stop = [body.stop];
+ }
+ }
+ const seed = clampNumeric(body.seed, 0, 4294967295);
+ if (seed !== null && seed > 0) payload.seed = Math.floor(seed);
+ if (body.include_ai_filters === true) payload.includeAiFilters = true;
+}
+
+/** Build the CLOVA Studio v3 request body from an OpenAI Chat Completions body. */
+export function buildClovaPayload(
+ model: string,
+ body: Record,
+ stream: boolean,
+ credentials?: Record | null
+): Record {
+ void stream;
+ void credentials;
+ const reasoning = isClovaReasoningModel(model);
+ const mode = resolveClovaMode(model, body);
+ const supportsImages = mode === "plain" && isClovaVisionModel(model);
+ const payload: JsonRecord = { messages: toClovaMessages(body, mode, supportsImages) };
+
+ applyThinking(payload, body, reasoning, mode);
+ applySampling(payload, body);
+ applyOutputCap(payload, body, reasoning, mode);
+ applyModeFields(payload, body, mode);
+ applyPlainOptions(payload, body, reasoning, mode);
+ return payload;
+}
+
+register(FORMATS.OPENAI, FORMATS.CLOVA, buildClovaPayload, null);
diff --git a/open-sse/translator/response/clova-to-openai.ts b/open-sse/translator/response/clova-to-openai.ts
new file mode 100644
index 0000000000..4d2b9cee54
--- /dev/null
+++ b/open-sse/translator/response/clova-to-openai.ts
@@ -0,0 +1,354 @@
+/**
+ * Naver CLOVA Studio "Chat Completions v3" → OpenAI response translator.
+ *
+ * CLOVA v3 streams as SSE with **named events**:
+ *
+ * ```
+ * id:
+ * event: token
+ * data: {"message":{"role":"assistant","content":"안"},"finishReason":null,...}
+ *
+ * id:
+ * event: result
+ * data: {"message":{"role":"assistant","content":"안녕"},"finishReason":"stop",
+ * "usage":{"promptTokens":20,"completionTokens":5,"totalTokens":25}}
+ * ```
+ *
+ * Three traps this translator exists to defuse:
+ *
+ * 1. **`event: token` carries an incremental delta, but `event: result` repeats
+ * the COMPLETE text.** Concatenating both duplicates the whole answer at the
+ * end of the stream, so the result event is treated as a terminal snapshot:
+ * it contributes `finish_reason` + `usage` only.
+ * 2. **Function-calling streams deliver arguments as `partialJson` fragments.**
+ * The first token carries the tool `id` + `name`; every later token carries
+ * only a JSON fragment (`{`, `"location`, `":`, ` "`, `Se`, `oul`, `"}`),
+ * which have to be reassembled into OpenAI's `tool_calls[].function.arguments`
+ * string. The terminal frame repeats the finished call, so — same rule as the
+ * text snapshot — it is not re-emitted.
+ * 3. **Failures can arrive as an in-stream payload** whose `status.code` is not
+ * `20000`, not just as an HTTP error. Those are surfaced through
+ * `state.upstreamError` so stream.ts fails the request out and combo fallback
+ * can run, mirroring the Gemini translator.
+ *
+ * Docs: https://api.ncloud-docs.com/docs/clovastudio-chatcompletionsv3
+ */
+import { register } from "../registry.ts";
+import { FORMATS } from "../formats.ts";
+
+/** CLOVA's success status code (a string, not an HTTP number). */
+const CLOVA_STATUS_OK = "20000";
+
+type JsonRecord = Record;
+
+interface ClovaStreamState extends JsonRecord {
+ responseId?: string;
+ created?: number;
+ model?: string;
+ chunkIndex?: number;
+ usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
+ upstreamError?: { status: number; type: string; code: string; message: string };
+ toolCallStarted?: boolean;
+ finishReason?: string;
+}
+
+function toRecord(value: unknown): JsonRecord | null {
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
+}
+
+/** Map a CLOVA `finishReason` onto the OpenAI vocabulary. */
+function mapFinishReason(reason: unknown): string {
+ switch (String(reason || "")) {
+ case "length":
+ return "length";
+ case "tool_calls":
+ return "tool_calls";
+ case "content_filter":
+ return "content_filter";
+ default:
+ return "stop";
+ }
+}
+
+/**
+ * Map a CLOVA string status code onto an HTTP status for error surfacing.
+ * Codes are 5-digit strings: `2xxxx` success, `4xxxx` client, `5xxxx` server.
+ */
+function httpStatusFromClovaCode(code: unknown): number {
+ const first = String(code || "").charAt(0);
+ if (first === "4") return 400;
+ return 502;
+}
+
+/**
+ * Parse one raw SSE frame into `{ event, data }`.
+ * CLOVA emits `id:` / `event:` / `data:` lines per frame.
+ */
+export function parseClovaSseFrame(raw: string): { event: string; data: unknown } | null {
+ if (typeof raw !== "string" || !raw.trim()) return null;
+
+ let event = "";
+ let dataLine = "";
+
+ for (const line of raw.split("\n")) {
+ const trimmed = line.trim();
+ if (trimmed.startsWith("event:")) {
+ event = trimmed.slice(6).trim();
+ } else if (trimmed.startsWith("data:")) {
+ dataLine = trimmed.slice(5).trim();
+ }
+ }
+
+ if (!dataLine) return null;
+
+ try {
+ return { event, data: JSON.parse(dataLine) };
+ } catch {
+ return null;
+ }
+}
+
+function baseChunk(state: ClovaStreamState): Record {
+ return {
+ id: state.responseId,
+ object: "chat.completion.chunk",
+ created: state.created,
+ model: state.model || "clova",
+ };
+}
+
+/**
+ * Build one OpenAI delta chunk.
+ *
+ * `field` selects the delta key: `"content"` for the visible answer and
+ * `"reasoning_content"` for CLOVA's `thinkingContent` (HCX-007).
+ */
+function deltaChunk(
+ state: ClovaStreamState,
+ content: string,
+ field = "content"
+): Record {
+ const chunk = baseChunk(state);
+ chunk.choices = [
+ {
+ index: 0,
+ delta: {
+ ...((state.chunkIndex ?? 0) === 0 ? { role: "assistant" } : {}),
+ [field]: content,
+ },
+ finish_reason: null,
+ },
+ ];
+ state.chunkIndex = (state.chunkIndex ?? 0) + 1;
+ return chunk;
+}
+
+/** First tool-call chunk: carries id + name and opens an empty argument string. */
+function toolCallStartChunk(
+ state: ClovaStreamState,
+ id: string,
+ name: string
+): Record {
+ const chunk = baseChunk(state);
+ chunk.choices = [
+ {
+ index: 0,
+ delta: {
+ ...((state.chunkIndex ?? 0) === 0 ? { role: "assistant" } : {}),
+ tool_calls: [
+ {
+ index: 0,
+ id: id || `call_${state.responseId}`,
+ type: "function",
+ function: { name, arguments: "" },
+ },
+ ],
+ },
+ finish_reason: null,
+ },
+ ];
+ state.chunkIndex = (state.chunkIndex ?? 0) + 1;
+ return chunk;
+}
+
+/** Subsequent tool-call chunk: appends one `partialJson` fragment. */
+function toolCallArgumentsChunk(
+ state: ClovaStreamState,
+ fragment: string
+): Record {
+ const chunk = baseChunk(state);
+ chunk.choices = [
+ {
+ index: 0,
+ delta: { tool_calls: [{ index: 0, function: { arguments: fragment } }] },
+ finish_reason: null,
+ },
+ ];
+ state.chunkIndex = (state.chunkIndex ?? 0) + 1;
+ return chunk;
+}
+
+function terminalChunk(state: ClovaStreamState, finishReason: string): Record {
+ const chunk = baseChunk(state);
+ chunk.choices = [{ index: 0, delta: {}, finish_reason: finishReason }];
+ if (state.usage) chunk.usage = state.usage;
+ return chunk;
+}
+
+function recordUsage(state: ClovaStreamState, usage: unknown): void {
+ const record = toRecord(usage);
+ if (!record) return;
+ const prompt = Number(record.promptTokens) || 0;
+ const completion = Number(record.completionTokens) || 0;
+ const total = Number(record.totalTokens) || prompt + completion;
+ state.usage = {
+ prompt_tokens: prompt,
+ completion_tokens: completion,
+ total_tokens: total,
+ };
+}
+
+function recordUpstreamError(state: ClovaStreamState, code: unknown, message: unknown): void {
+ const status = httpStatusFromClovaCode(code);
+ state.upstreamError = {
+ status,
+ type: status === 429 ? "rate_limit_error" : "server_error",
+ code: String(code || "clova_error"),
+ message: typeof message === "string" && message ? message : "CLOVA Studio upstream failure",
+ };
+}
+
+interface DecodedClovaChunk {
+ event: string;
+ data: JsonRecord;
+}
+
+function initializeState(state: ClovaStreamState): void {
+ if (state.responseId) return;
+ state.responseId = `chatcmpl-${Date.now()}`;
+ state.created = Math.floor(Date.now() / 1000);
+ state.chunkIndex = 0;
+}
+
+function decodeClovaChunk(chunk: unknown): DecodedClovaChunk | null {
+ if (typeof chunk === "string") {
+ const frame = parseClovaSseFrame(chunk);
+ const data = toRecord(frame?.data);
+ return frame && data ? { event: frame.event, data } : null;
+ }
+ const data = toRecord(chunk);
+ if (!data) return null;
+ return { event: String(data.event || data._eventType || ""), data };
+}
+
+function handleErrorEnvelope(state: ClovaStreamState, event: string, data: JsonRecord): boolean {
+ const status = toRecord(data.status);
+ const statusCode = status?.code ?? data.statusCode;
+ if (statusCode != null && String(statusCode) !== CLOVA_STATUS_OK) {
+ recordUpstreamError(state, statusCode, status?.message ?? data.message);
+ return true;
+ }
+
+ const error = toRecord(data.error);
+ if (event !== "error" && !error) return false;
+ const source = error ?? data;
+ const errorStatus = toRecord(source.status);
+ recordUpstreamError(
+ state,
+ errorStatus?.code ?? source.code,
+ errorStatus?.message ?? source.message
+ );
+ return true;
+}
+
+function toolCallDelta(state: ClovaStreamState, call: unknown): Record | null {
+ const record = toRecord(call);
+ const fn = toRecord(record?.function);
+ if (!record || !fn) return null;
+ const id = typeof record.id === "string" ? record.id : "";
+ const name = typeof fn.name === "string" ? fn.name : "";
+ if (id || name) {
+ if (state.toolCallStarted) return null;
+ state.toolCallStarted = true;
+ return toolCallStartChunk(state, id, name);
+ }
+ return typeof fn.partialJson === "string" && fn.partialJson
+ ? toolCallArgumentsChunk(state, fn.partialJson)
+ : null;
+}
+
+function toolCallDeltas(
+ state: ClovaStreamState,
+ message: JsonRecord
+): Record | Array> | null {
+ if (!Array.isArray(message.toolCalls) || message.toolCalls.length === 0) return null;
+ const out = message.toolCalls
+ .map((call) => toolCallDelta(state, call))
+ .filter((chunk): chunk is Record => chunk !== null);
+ if (out.length === 0) return null;
+ return out.length === 1 ? out[0] : out;
+}
+
+function convertTokenEvent(
+ state: ClovaStreamState,
+ data: JsonRecord
+): Record | Array> | null {
+ const message = toRecord(data.message) ?? data;
+ const toolDeltas = toolCallDeltas(state, message);
+ if (toolDeltas) return toolDeltas;
+ const thinking = message.thinkingContent ?? data.thinkingContent;
+ if (thinking) return deltaChunk(state, String(thinking), "reasoning_content");
+ const content = message.content ?? data.content;
+ return content ? deltaChunk(state, String(content)) : null;
+}
+
+function shouldEmitResultSnapshot(
+ state: ClovaStreamState,
+ isResultEvent: boolean,
+ snapshot: unknown
+): snapshot is string {
+ return (
+ !isResultEvent && (state.chunkIndex ?? 0) === 0 && typeof snapshot === "string" && !!snapshot
+ );
+}
+
+function convertResultEvent(
+ state: ClovaStreamState,
+ event: string,
+ data: JsonRecord
+): Record | Array> | null {
+ const isResultEvent = event === "result" || event === "stop";
+ const resultEnvelope = toRecord(data.result);
+ if (!isResultEvent && (event || !resultEnvelope)) return null;
+
+ const result = resultEnvelope ?? data;
+ const message = toRecord(result.message);
+ recordUsage(state, result.usage);
+ const hasToolCalls = Array.isArray(message?.toolCalls) && message.toolCalls.length > 0;
+ const finishReason = hasToolCalls ? "tool_calls" : mapFinishReason(result.finishReason);
+ state.finishReason = finishReason;
+
+ const snapshot = message?.content ?? result.content;
+ if (shouldEmitResultSnapshot(state, isResultEvent, snapshot)) {
+ return [deltaChunk(state, snapshot), terminalChunk(state, finishReason)];
+ }
+ return terminalChunk(state, finishReason);
+}
+
+/** Convert one CLOVA stream frame or JSON envelope into OpenAI chunk(s). */
+export function convertClovaToOpenAI(
+ chunk: unknown,
+ state: Record
+): Record | Array> | null {
+ if (chunk == null) return null;
+ const streamState = state as ClovaStreamState;
+ initializeState(streamState);
+ const decoded = decodeClovaChunk(chunk);
+ if (!decoded) return null;
+ if (handleErrorEnvelope(streamState, decoded.event, decoded.data)) return null;
+ return decoded.event === "token"
+ ? convertTokenEvent(streamState, decoded.data)
+ : convertResultEvent(streamState, decoded.event, decoded.data);
+}
+
+register(FORMATS.CLOVA, FORMATS.OPENAI, null, convertClovaToOpenAI);
diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts
index 4d255d40be..a2244f01f5 100644
--- a/open-sse/translator/response/openai-responses.ts
+++ b/open-sse/translator/response/openai-responses.ts
@@ -23,12 +23,12 @@ import {
import { createEventEmitter } from "./openai-responses/eventEmitter.ts";
import { buildResponsesToolCallItem } from "./responsesToolItem.ts";
import { resolveRequestToolIdentity } from "./openai-responses/requestToolIdentity.ts";
+import { resolveLocalToolCallIndex } from "./openai-responses/toolCallLocalIndex.ts";
import {
synthesizeCompletedToolCalls,
computeFinishReason,
withAssistantRoleOnFirstDelta,
} from "./openai-responses/synthesizeCompletedToolCalls.ts";
-
// normalizeUpstreamFailure is re-exported for external importers (tests).
export { normalizeUpstreamFailure } from "./openai-responses/pureHelpers.ts";
@@ -506,7 +506,7 @@ function toolCallOutputIndexBase(state) {
function emitToolCall(state, emit, tc) {
const tcIdx = tc.index ?? 0;
- const outputIndex = toolCallOutputIndexBase(state) + normalizeOutputIndex(tcIdx);
+ const outputIndex = toolCallOutputIndexBase(state) + resolveLocalToolCallIndex(state, tcIdx);
const newCallId = tc.id;
const funcName = tc.function?.name;
@@ -609,7 +609,7 @@ function emitToolCall(state, emit, tc) {
function closeToolCall(state, emit, idx, recordAsCompleted = true) {
const callId = state.funcCallIds[idx];
if (callId && !state.funcItemDone[idx]) {
- const normalizedIndex = toolCallOutputIndexBase(state) + normalizeOutputIndex(idx);
+ const normalizedIndex = toolCallOutputIndexBase(state) + resolveLocalToolCallIndex(state, idx);
const args = state.funcArgsBuf[idx] || "{}";
const toolName = state.funcNames[idx] || "";
// See emitToolCall()'s isCustomTool comment — must stay in sync (both compute the
diff --git a/open-sse/translator/response/openai-responses/toolCallLocalIndex.ts b/open-sse/translator/response/openai-responses/toolCallLocalIndex.ts
new file mode 100644
index 0000000000..645278e1fd
--- /dev/null
+++ b/open-sse/translator/response/openai-responses/toolCallLocalIndex.ts
@@ -0,0 +1,35 @@
+/**
+ * Remap a turn's raw upstream tool_calls delta `index` onto a local,
+ * contiguous, 0-based sequence in first-seen order.
+ *
+ * Live incident (2026-09-02, minimax-m3:free via OpenRouter/GMICloud): the
+ * upstream's own `index` doesn't reliably start at 0 or stay contiguous per
+ * turn — this turn's two calls arrived with raw index 1 and 2 (never 0).
+ * Adding that raw index straight onto toolCallOutputIndexBase() left a GAP
+ * in the emitted output_index sequence (0 for the message, then 2 and 3 for
+ * the calls — index 1 never used). A client that reads response.completed's
+ * final `output[]` array by ARRAY POSITION and expects position to equal
+ * output_index (the Responses API's own contract) reads output[1] (this
+ * turn's first call, real output_index 2) while looking it up under
+ * output_index 1, misses it, then reads output[2] (the second call, real
+ * output_index 3) under output_index 2 — landing on the FIRST call's tracked
+ * slot with a different call_id, which a spec-following client correctly
+ * treats as "stream changed output item identity" and aborts.
+ */
+
+export type ToolCallLocalIndexState = {
+ toolCallLocalIndex?: Record;
+ toolCallLocalIndexNext?: number;
+};
+
+export function resolveLocalToolCallIndex(
+ state: ToolCallLocalIndexState,
+ tcIdx: string | number
+): number {
+ if (!state.toolCallLocalIndex) state.toolCallLocalIndex = {};
+ if (state.toolCallLocalIndex[tcIdx] === undefined) {
+ state.toolCallLocalIndex[tcIdx] = state.toolCallLocalIndexNext ?? 0;
+ state.toolCallLocalIndexNext = state.toolCallLocalIndex[tcIdx] + 1;
+ }
+ return state.toolCallLocalIndex[tcIdx];
+}
diff --git a/open-sse/utils/composerToolCalls.ts b/open-sse/utils/composerToolCalls.ts
index 916903a3ca..40d2c306af 100644
--- a/open-sse/utils/composerToolCalls.ts
+++ b/open-sse/utils/composerToolCalls.ts
@@ -48,6 +48,18 @@ const INNER_RE = new RegExp(
// Match an arg separator.
const ARG_SEP_RE = new RegExp(`<${FW}tool${SEP}sep${FW}>`, "gi");
+// Opening-only marker, matched on every streamed delta in the holdback path;
+// kept as a module constant so it is compiled once instead of per call.
+const OPEN_ONLY_RE = new RegExp(`<${FW}tool${SEP}calls${SEP}begin${FW}>`, "i");
+
+// Parse helpers below run once per tool-call block / per argument value during
+// streaming, so their literals are hoisted too.
+const TRIM_EDGES_RE = /^\s+|\s+$/g;
+const FIRST_SPACE_RE = /\s/;
+const TRAILING_NEWLINES_RE = /\n+$/;
+const INTEGER_RE = /^-?\d+$/;
+const DECIMAL_RE = /^-?\d*\.\d+$/;
+
// Heuristic: any partial opening marker (start of `<|tool` ... without the
// final `>`). Used by the streaming parser to know it must hold back text.
const PARTIAL_OPEN_MARKER_RE = new RegExp(
@@ -115,7 +127,7 @@ function generateToolCallId(index: number): string {
function parseInnerCall(body: string): { name: string; arguments: string } | null {
// Body starts with the tool name on (typically) its own line, optionally
// surrounded by whitespace, then the first `<|tool▁sep|>`.
- const trimmed = body.replace(/^\s+|\s+$/g, "");
+ const trimmed = body.replace(TRIM_EDGES_RE, "");
// Split by argument separator first to isolate name + arg blocks.
const segments = trimmed.split(ARG_SEP_RE);
// First segment is the tool name (and any preamble whitespace).
@@ -137,7 +149,7 @@ function parseInnerCall(body: string): { name: string; arguments: string } | nul
let argName: string;
let argValue: string;
if (idxNl < 0) {
- const idxSp = seg.search(/\s/);
+ const idxSp = seg.search(FIRST_SPACE_RE);
if (idxSp < 0) {
argName = seg.trim();
argValue = "";
@@ -155,7 +167,7 @@ function parseInnerCall(body: string): { name: string; arguments: string } | nul
if (!argName) continue;
// Strip the trailing newline before the next separator (the separator
// marker itself was already consumed by the split).
- argValue = argValue.replace(/\n+$/, "");
+ argValue = argValue.replace(TRAILING_NEWLINES_RE, "");
// Attempt JSON parse so structured args (objects/arrays/numbers/bools)
// come through as native JSON values rather than quoted strings.
args[argName] = coerceArgValue(argValue);
@@ -179,11 +191,11 @@ function coerceArgValue(raw: string): unknown {
if (stripped === "true") return true;
if (stripped === "false") return false;
if (stripped === "null") return null;
- if (/^-?\d+$/.test(stripped)) {
+ if (INTEGER_RE.test(stripped)) {
const n = Number(stripped);
if (Number.isSafeInteger(n)) return n;
}
- if (/^-?\d*\.\d+$/.test(stripped)) {
+ if (DECIMAL_RE.test(stripped)) {
const n = Number(stripped);
if (Number.isFinite(n)) return n;
}
@@ -295,8 +307,7 @@ export function feedStreamingChunk(state: StreamingState, accumulated: string):
// 2. Look for an opening-only marker. If found, everything before it is
// safe; everything after must be held until we see the closing marker.
- const openOnlyRe = new RegExp(`<${FW}tool${SEP}calls${SEP}begin${FW}>`, "i");
- const openMatch = accumulated.match(openOnlyRe);
+ const openMatch = accumulated.match(OPEN_ONLY_RE);
if (openMatch && openMatch.index !== undefined) {
const safe = accumulated.slice(0, openMatch.index);
const safeDelta = safe.length > state.emitted ? safe.slice(state.emitted) : "";
diff --git a/open-sse/utils/cursorAgentProtobuf.ts b/open-sse/utils/cursorAgentProtobuf.ts
index 21164b6ed1..0e3f642682 100644
--- a/open-sse/utils/cursorAgentProtobuf.ts
+++ b/open-sse/utils/cursorAgentProtobuf.ts
@@ -24,6 +24,10 @@ import {
encodeSelectedImageBody,
type EncodedImage,
} from "./cursorAgentProtobuf/imageEncoding.ts";
+import {
+ CURSOR_EFFORT_SUFFIXES,
+ resolveOneMillionContextModel,
+} from "./cursorAgentProtobuf/requestedModelParameters.ts";
import {
WT_VARINT,
WT_LEN,
@@ -41,6 +45,7 @@ import {
findField,
decodeStringField,
decodeVarintField,
+ type Field,
} from "./cursorAgentProtobuf/wire.ts";
// ─── Field numbers (from agent.proto descriptor) ───────────────────────────
@@ -311,8 +316,6 @@ export function normalizeCursorModelId(modelId: string): string {
// Grok (`cursor-grok-*` / legacy `grok-*`) follows the Claude-style `effort`
// parameter. Without the split, ids like `cursor-grok-4.5-high` return empty
// turns (same symptom as #7289). Combined `-high-fast` is supported.
-const CURSOR_EFFORT_SUFFIXES = ["low", "medium", "high", "xhigh", "max"] as const;
-
/**
* If `normalized` starts with `prefix` and ends with one of the known effort
* suffixes, split it into the base model id plus a `{id: paramId, value}`
@@ -432,6 +435,8 @@ export function resolveRequestedModel(
};
}
}
+ const oneMillionContext = resolveOneMillionContextModel(normalized);
+ if (oneMillionContext) return oneMillionContext;
// Live catalog is authoritative for exact ids (flattened effort variants).
if (opts?.liveCatalogIds?.has(normalized)) {
return { modelId: normalized, parameters: [] };
@@ -652,6 +657,41 @@ export type DecodedDelta =
| { kind: "kv_server_message" }
| { kind: "unknown"; field: number };
+type InteractionUpdateDecoder = (field: Field) => DecodedDelta[];
+
+const INTERACTION_UPDATE_DECODERS: Partial> = {
+ [IU_TEXT_DELTA]: (field) =>
+ field.wireType === WT_LEN
+ ? [{ kind: "text", text: decodeStringField(field.bytes, TDU_TEXT) }]
+ : [],
+ [IU_THINKING_DELTA]: (field) =>
+ field.wireType === WT_LEN
+ ? [{ kind: "thinking", text: decodeStringField(field.bytes, TDU_TEXT) }]
+ : [],
+ [IU_THINKING_COMPLETED]: () => [{ kind: "thinking_complete" }],
+ [IU_TOOL_CALL_STARTED]: () => [{ kind: "tool_call_started" }],
+ [IU_TOOL_CALL_COMPLETED]: (field) => {
+ const deltas: DecodedDelta[] = [];
+ if (field.wireType === WT_LEN) {
+ const todoWrite = decodeNativeTodoWriteCompletion(field.bytes);
+ if (todoWrite) deltas.push(todoWrite);
+ }
+ deltas.push({ kind: "tool_call_completed" });
+ return deltas;
+ },
+ [IU_TOKEN_DELTA]: (field) =>
+ field.wireType === WT_LEN
+ ? [{ kind: "token_delta", tokens: decodeVarintField(field.bytes, 1) }]
+ : [],
+ [IU_HEARTBEAT]: () => [{ kind: "heartbeat" }],
+ [IU_TURN_ENDED]: () => [{ kind: "turn_ended" }],
+};
+
+function decodeInteractionUpdate(field: Field): DecodedDelta[] {
+ const decoder = INTERACTION_UPDATE_DECODERS[field.fieldNumber];
+ return decoder ? decoder(field) : [{ kind: "unknown", field: field.fieldNumber }];
+}
+
export function decodeAgentServerMessage(payload: Buffer): DecodedDelta[] {
const out: DecodedDelta[] = [];
for (const top of decodeFields(payload)) {
@@ -661,45 +701,7 @@ export function decodeAgentServerMessage(payload: Buffer): DecodedDelta[] {
}
if (top.fieldNumber !== ASM_INTERACTION_UPDATE || top.wireType !== 2) continue;
for (const update of decodeFields(top.bytes)) {
- if (update.wireType !== 2 && update.wireType !== 0) continue;
- switch (update.fieldNumber) {
- case IU_TEXT_DELTA:
- if (update.wireType === 2) {
- out.push({ kind: "text", text: decodeStringField(update.bytes, TDU_TEXT) });
- }
- break;
- case IU_THINKING_DELTA:
- if (update.wireType === 2) {
- out.push({ kind: "thinking", text: decodeStringField(update.bytes, TDU_TEXT) });
- }
- break;
- case IU_THINKING_COMPLETED:
- out.push({ kind: "thinking_complete" });
- break;
- case IU_TOOL_CALL_STARTED:
- out.push({ kind: "tool_call_started" });
- break;
- case IU_TOOL_CALL_COMPLETED:
- if (update.wireType === 2) {
- const todoWrite = decodeNativeTodoWriteCompletion(update.bytes);
- if (todoWrite) out.push(todoWrite);
- }
- out.push({ kind: "tool_call_completed" });
- break;
- case IU_TOKEN_DELTA:
- if (update.wireType === 2) {
- out.push({ kind: "token_delta", tokens: decodeVarintField(update.bytes, 1) });
- }
- break;
- case IU_HEARTBEAT:
- out.push({ kind: "heartbeat" });
- break;
- case IU_TURN_ENDED:
- out.push({ kind: "turn_ended" });
- break;
- default:
- out.push({ kind: "unknown", field: update.fieldNumber });
- }
+ out.push(...decodeInteractionUpdate(update));
}
}
return out;
@@ -750,52 +752,44 @@ export type KvServerEvent =
requestMetadata: Buffer | null;
};
+function findLengthDelimitedField(fields: Field[], fieldNumber: number): Buffer | null {
+ const field = findField(fields, fieldNumber);
+ return field?.wireType === WT_LEN ? field.bytes : null;
+}
+
+function decodeBlobId(payload: Buffer, fieldNumber: number): Buffer {
+ return findLengthDelimitedField(decodeFields(payload), fieldNumber) ?? Buffer.alloc(0);
+}
+
+function decodeSetBlobArgs(payload: Buffer): { blobId: Buffer; blobData: Buffer } {
+ const fields = decodeFields(payload);
+ return {
+ blobId: findLengthDelimitedField(fields, SBA_BLOB_ID) ?? Buffer.alloc(0),
+ blobData: findLengthDelimitedField(fields, SBA_BLOB_DATA) ?? Buffer.alloc(0),
+ };
+}
+
export function decodeKvServerEvent(payload: Buffer): KvServerEvent | null {
- for (const top of decodeFields(payload)) {
- if (top.fieldNumber !== ASM_KV_SERVER_MESSAGE || top.wireType !== 2) continue;
+ const top = findField(decodeFields(payload), ASM_KV_SERVER_MESSAGE);
+ if (top?.wireType !== WT_LEN) return null;
- let kvId = 0;
- let getBlobArgs: Buffer | null = null;
- let setBlobArgs: Buffer | null = null;
- let requestMetadata: Buffer | null = null;
-
- for (const f of decodeFields(top.bytes)) {
- if (f.fieldNumber === KSM_ID && f.wireType === 0) {
- kvId = Number(f.varint);
- } else if (f.fieldNumber === KSM_GET_BLOB_ARGS && f.wireType === 2) {
- getBlobArgs = f.bytes;
- } else if (f.fieldNumber === KSM_SET_BLOB_ARGS && f.wireType === 2) {
- setBlobArgs = f.bytes;
- } else if (f.fieldNumber === KSM_REQUEST_METADATA && f.wireType === 2) {
- requestMetadata = f.bytes;
- }
- }
-
- if (getBlobArgs) {
- // GetBlobArgs { blob_id (1): bytes }
- let blobId: Buffer = Buffer.alloc(0);
- for (const f of decodeFields(getBlobArgs)) {
- if (f.fieldNumber === GBA_BLOB_ID && f.wireType === 2) {
- blobId = f.bytes;
- }
- }
- return { kind: "kv_get_blob", kvId, blobId, requestMetadata };
- }
- if (setBlobArgs) {
- // SetBlobArgs { blob_id (1): bytes, blob_data (2): bytes }
- let blobId: Buffer = Buffer.alloc(0);
- let blobData: Buffer = Buffer.alloc(0);
- for (const f of decodeFields(setBlobArgs)) {
- if (f.fieldNumber === SBA_BLOB_ID && f.wireType === 2) {
- blobId = f.bytes;
- } else if (f.fieldNumber === SBA_BLOB_DATA && f.wireType === 2) {
- blobData = f.bytes;
- }
- }
- return { kind: "kv_set_blob", kvId, blobId, blobData, requestMetadata };
- }
+ const fields = decodeFields(top.bytes);
+ const idField = findField(fields, KSM_ID);
+ const kvId = idField?.wireType === WT_VARINT ? Number(idField.varint) : 0;
+ const requestMetadata = findLengthDelimitedField(fields, KSM_REQUEST_METADATA);
+ const getBlobArgs = findLengthDelimitedField(fields, KSM_GET_BLOB_ARGS);
+ if (getBlobArgs) {
+ return {
+ kind: "kv_get_blob",
+ kvId,
+ blobId: decodeBlobId(getBlobArgs, GBA_BLOB_ID),
+ requestMetadata,
+ };
}
- return null;
+
+ const setBlobArgs = findLengthDelimitedField(fields, KSM_SET_BLOB_ARGS);
+ if (!setBlobArgs) return null;
+ return { kind: "kv_set_blob", kvId, ...decodeSetBlobArgs(setBlobArgs), requestMetadata };
}
// ─── Phase 2: full ExecServerMessage variant decoder ───────────────────────
@@ -886,143 +880,121 @@ function decodeShellArgs(payload: Buffer): DecodedShellArgs {
return decoded;
}
-export function decodeExecServerEvent(payload: Buffer): ExecServerEvent | null {
- for (const top of decodeFields(payload)) {
- if (top.fieldNumber !== ASM_EXEC_SERVER_MESSAGE || top.wireType !== 2) continue;
+type ExecEventContext = {
+ execMsgId: number;
+ execId: string;
+ variantBytes: Buffer;
+};
- let execMsgId = 0;
- let execId = "";
- let variantField = 0;
- let variantBytes: Buffer | null = null;
+type ExecEventDecoder = (context: ExecEventContext) => ExecServerEvent;
+type PathExecKind = "exec_read" | "exec_write" | "exec_delete" | "exec_ls";
+type ShellExecKind = "exec_shell" | "exec_shell_stream" | "exec_bg_shell";
- for (const f of decodeFields(top.bytes)) {
- if (f.fieldNumber === ESM_ID && f.wireType === 0) {
- execMsgId = Number(f.varint);
- } else if (f.fieldNumber === ESM_EXEC_ID && f.wireType === 2) {
- execId = f.bytes.toString("utf8");
- } else if (f.wireType === 2) {
- // Any other LEN field is the variant payload. Take the first one we
- // see — variants don't co-occur in a well-formed message.
- if (variantField === 0) {
- variantField = f.fieldNumber;
- variantBytes = f.bytes;
- }
- }
- }
+function createPathExecEvent(kind: PathExecKind, context: ExecEventContext): ExecServerEvent {
+ return {
+ kind,
+ execMsgId: context.execMsgId,
+ execId: context.execId,
+ path: decodeStringField(context.variantBytes, ARG_PATH),
+ };
+}
- if (variantBytes === null) continue;
+function createShellExecEvent(kind: ShellExecKind, context: ExecEventContext): ExecServerEvent {
+ return {
+ kind,
+ execMsgId: context.execMsgId,
+ execId: context.execId,
+ ...decodeShellArgs(context.variantBytes),
+ };
+}
- switch (variantField) {
- case ESM_REQUEST_CONTEXT_ARGS:
- return { kind: "exec_request_context", execMsgId, execId };
- case ESM_READ_ARGS:
- return {
- kind: "exec_read",
- execMsgId,
- execId,
- path: decodeStringField(variantBytes, ARG_PATH),
- };
- case ESM_WRITE_ARGS:
- return {
- kind: "exec_write",
- execMsgId,
- execId,
- path: decodeStringField(variantBytes, ARG_PATH),
- };
- case ESM_DELETE_ARGS:
- return {
- kind: "exec_delete",
- execMsgId,
- execId,
- path: decodeStringField(variantBytes, ARG_PATH),
- };
- case ESM_LS_ARGS:
- return {
- kind: "exec_ls",
- execMsgId,
- execId,
- path: decodeStringField(variantBytes, ARG_PATH),
- };
- case ESM_GREP_ARGS:
- return { kind: "exec_grep", execMsgId, execId };
- case ESM_DIAGNOSTICS_ARGS:
- return { kind: "exec_diagnostics", execMsgId, execId };
- case ESM_SHELL_ARGS: {
- const shell = decodeShellArgs(variantBytes);
- return {
- kind: "exec_shell",
- execMsgId,
- execId,
- ...shell,
- };
- }
- case ESM_SHELL_STREAM_ARGS: {
- const shell = decodeShellArgs(variantBytes);
- return {
- kind: "exec_shell_stream",
- execMsgId,
- execId,
- ...shell,
- };
- }
- case ESM_BACKGROUND_SHELL_SPAWN: {
- const shell = decodeShellArgs(variantBytes);
- return {
- kind: "exec_bg_shell",
- execMsgId,
- execId,
- ...shell,
- };
- }
- case ESM_FETCH_ARGS:
- return {
- kind: "exec_fetch",
- execMsgId,
- execId,
- url: decodeStringField(variantBytes, ARG_FETCH_URL),
- };
- case ESM_WRITE_SHELL_STDIN_ARGS:
- return { kind: "exec_write_shell_stdin", execMsgId, execId };
- case ESM_MCP_ARGS: {
- // McpArgs.args is map; each value is a protobuf-
- // encoded google.protobuf.Value. Decode keys and value-bytes here,
- // then convert each Value to its JSON shape.
- let toolName = "";
- let toolCallId = "";
- const args: Record = {};
- for (const f of decodeFields(variantBytes)) {
- if (f.wireType !== 2) continue;
- if (f.fieldNumber === MCA_TOOL_NAME) {
- toolName = f.bytes.toString("utf8");
- } else if (f.fieldNumber === MCA_NAME && !toolName) {
- // tool_name (5) takes precedence; fall back to name (1)
- toolName = f.bytes.toString("utf8");
- } else if (f.fieldNumber === MCA_TOOL_CALL_ID) {
- toolCallId = f.bytes.toString("utf8");
- } else if (f.fieldNumber === MCA_ARGS) {
- // FieldsEntry { key (1): string, value (2): bytes }
- let key = "";
- let valueBytes: Buffer | null = null;
- for (const entry of decodeFields(f.bytes)) {
- if (entry.fieldNumber === MAP_KEY && entry.wireType === 2) {
- key = entry.bytes.toString("utf8");
- } else if (entry.fieldNumber === MAP_VALUE && entry.wireType === 2) {
- valueBytes = entry.bytes;
- }
- }
- if (key && valueBytes !== null) {
- args[key] = decodeProtobufValue(valueBytes);
- }
- }
- }
- return { kind: "exec_mcp", execMsgId, execId, toolName, toolCallId, args };
- }
- default:
- // Unknown variant — return null so caller can keep buffering.
- return null;
- }
+function decodeMcpMapEntry(payload: Buffer): { key: string; value: unknown } | null {
+ const fields = decodeFields(payload);
+ const key = findLengthDelimitedField(fields, MAP_KEY)?.toString("utf8") ?? "";
+ const valueBytes = findLengthDelimitedField(fields, MAP_VALUE);
+ return key && valueBytes ? { key, value: decodeProtobufValue(valueBytes) } : null;
+}
+
+function decodeMcpExecEvent(context: ExecEventContext): ExecServerEvent {
+ const fields = decodeFields(context.variantBytes);
+ const canonicalName = findLengthDelimitedField(fields, MCA_TOOL_NAME);
+ const fallbackName = findLengthDelimitedField(fields, MCA_NAME);
+ const toolName = (canonicalName ?? fallbackName)?.toString("utf8") ?? "";
+ const toolCallId = findLengthDelimitedField(fields, MCA_TOOL_CALL_ID)?.toString("utf8") ?? "";
+ const args: Record = {};
+ for (const field of fields) {
+ if (field.fieldNumber !== MCA_ARGS || field.wireType !== WT_LEN) continue;
+ const entry = decodeMcpMapEntry(field.bytes);
+ if (entry) args[entry.key] = entry.value;
}
- return null;
+ return {
+ kind: "exec_mcp",
+ execMsgId: context.execMsgId,
+ execId: context.execId,
+ toolName,
+ toolCallId,
+ args,
+ };
+}
+
+const EXEC_EVENT_DECODERS: Partial> = {
+ [ESM_REQUEST_CONTEXT_ARGS]: ({ execMsgId, execId }) => ({
+ kind: "exec_request_context",
+ execMsgId,
+ execId,
+ }),
+ [ESM_READ_ARGS]: (context) => createPathExecEvent("exec_read", context),
+ [ESM_WRITE_ARGS]: (context) => createPathExecEvent("exec_write", context),
+ [ESM_DELETE_ARGS]: (context) => createPathExecEvent("exec_delete", context),
+ [ESM_LS_ARGS]: (context) => createPathExecEvent("exec_ls", context),
+ [ESM_GREP_ARGS]: ({ execMsgId, execId }) => ({ kind: "exec_grep", execMsgId, execId }),
+ [ESM_DIAGNOSTICS_ARGS]: ({ execMsgId, execId }) => ({
+ kind: "exec_diagnostics",
+ execMsgId,
+ execId,
+ }),
+ [ESM_SHELL_ARGS]: (context) => createShellExecEvent("exec_shell", context),
+ [ESM_SHELL_STREAM_ARGS]: (context) => createShellExecEvent("exec_shell_stream", context),
+ [ESM_BACKGROUND_SHELL_SPAWN]: (context) => createShellExecEvent("exec_bg_shell", context),
+ [ESM_FETCH_ARGS]: ({ execMsgId, execId, variantBytes }) => ({
+ kind: "exec_fetch",
+ execMsgId,
+ execId,
+ url: decodeStringField(variantBytes, ARG_FETCH_URL),
+ }),
+ [ESM_WRITE_SHELL_STDIN_ARGS]: ({ execMsgId, execId }) => ({
+ kind: "exec_write_shell_stdin",
+ execMsgId,
+ execId,
+ }),
+ [ESM_MCP_ARGS]: decodeMcpExecEvent,
+};
+
+function decodeExecEventContext(
+ payload: Buffer
+): (ExecEventContext & { variantField: number }) | null {
+ const top = findField(decodeFields(payload), ASM_EXEC_SERVER_MESSAGE);
+ if (top?.wireType !== WT_LEN) return null;
+
+ const fields = decodeFields(top.bytes);
+ const idField = findField(fields, ESM_ID);
+ const variant = fields.find(
+ (field) => field.wireType === WT_LEN && field.fieldNumber !== ESM_EXEC_ID
+ );
+ if (!variant || variant.wireType !== WT_LEN) return null;
+ return {
+ execMsgId: idField?.wireType === WT_VARINT ? Number(idField.varint) : 0,
+ execId: findLengthDelimitedField(fields, ESM_EXEC_ID)?.toString("utf8") ?? "",
+ variantField: variant.fieldNumber,
+ variantBytes: variant.bytes,
+ };
+}
+
+export function decodeExecServerEvent(payload: Buffer): ExecServerEvent | null {
+ const context = decodeExecEventContext(payload);
+ if (!context) return null;
+ const decoder = EXEC_EVENT_DECODERS[context.variantField];
+ return decoder?.(context) ?? null;
}
/**
@@ -1316,6 +1288,87 @@ export function jsonSchemaToProtobufValue(json: unknown): Buffer {
* Handles all six Value variants: null, number (double), string, bool,
* struct (object), list (array). Unknown fields are skipped.
*/
+type ProtobufValueDecodeResult = { value: unknown; nextPos: number };
+type ProtobufValueDecoder = (
+ buf: Buffer,
+ pos: number,
+ wireType: number
+) => ProtobufValueDecodeResult;
+
+function readLengthDelimitedPayload(
+ buf: Buffer,
+ pos: number,
+ wireType: number
+): { payload: Buffer; nextPos: number } | null {
+ if (wireType !== WT_LEN) return null;
+ const [len, afterLength] = decodeVarint(buf, pos);
+ const lenN = checkedLen(len, afterLength, buf);
+ return {
+ payload: buf.subarray(afterLength, afterLength + lenN),
+ nextPos: afterLength + lenN,
+ };
+}
+
+function decodeNullValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult {
+ const nextPos = wireType === WT_VARINT ? decodeVarint(buf, pos)[1] : pos;
+ return { value: null, nextPos };
+}
+
+function decodeNumberValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult {
+ const valid = wireType === 1 && pos + 8 <= buf.length;
+ return { value: valid ? buf.readDoubleLE(pos) : 0, nextPos: valid ? pos + 8 : pos };
+}
+
+function decodeStringValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult {
+ const decoded = readLengthDelimitedPayload(buf, pos, wireType);
+ return {
+ value: decoded?.payload.toString("utf8") ?? "",
+ nextPos: decoded?.nextPos ?? pos,
+ };
+}
+
+function decodeBoolValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult {
+ if (wireType !== WT_VARINT) return { value: false, nextPos: pos };
+ const [value, nextPos] = decodeVarint(buf, pos);
+ return { value: value !== 0n, nextPos };
+}
+
+function decodeStructValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult {
+ const decoded = readLengthDelimitedPayload(buf, pos, wireType);
+ return {
+ value: decoded ? decodeProtobufStruct(decoded.payload) : {},
+ nextPos: decoded?.nextPos ?? pos,
+ };
+}
+
+function decodeListValue(buf: Buffer, pos: number, wireType: number): ProtobufValueDecodeResult {
+ const decoded = readLengthDelimitedPayload(buf, pos, wireType);
+ return {
+ value: decoded ? decodeProtobufList(decoded.payload) : [],
+ nextPos: decoded?.nextPos ?? pos,
+ };
+}
+
+const PROTOBUF_VALUE_DECODERS: Partial> = {
+ [VAL_NULL]: decodeNullValue,
+ [VAL_NUMBER]: decodeNumberValue,
+ [VAL_STRING]: decodeStringValue,
+ [VAL_BOOL]: decodeBoolValue,
+ [VAL_STRUCT]: decodeStructValue,
+ [VAL_LIST]: decodeListValue,
+};
+
+function skipUnknownProtobufField(buf: Buffer, pos: number, wireType: number): number {
+ if (wireType === WT_VARINT) return decodeVarint(buf, pos)[1];
+ if (wireType === WT_LEN) {
+ const [len, afterLength] = decodeVarint(buf, pos);
+ return afterLength + checkedLen(len, afterLength, buf);
+ }
+ if (wireType === 1) return pos + 8;
+ if (wireType === 5) return pos + 4;
+ return pos;
+}
+
export function decodeProtobufValue(buf: Buffer): unknown {
let pos = 0;
while (pos < buf.length) {
@@ -1323,97 +1376,26 @@ export function decodeProtobufValue(buf: Buffer): unknown {
pos = np;
const fieldNumber = Number(t >> 3n);
const wireType = Number(t & 0x7n);
- switch (fieldNumber) {
- case VAL_NULL: {
- if (wireType === WT_VARINT) {
- [, pos] = decodeVarint(buf, pos);
- }
- return null;
- }
- case VAL_NUMBER: {
- if (wireType === 1 && pos + 8 <= buf.length) {
- const value = buf.readDoubleLE(pos);
- pos += 8;
- return value;
- }
- return 0;
- }
- case VAL_STRING: {
- if (wireType === WT_LEN) {
- const [len, np2] = decodeVarint(buf, pos);
- pos = np2;
- const lenN = checkedLen(len, pos, buf);
- const value = buf.subarray(pos, pos + lenN).toString("utf8");
- pos += lenN;
- return value;
- }
- return "";
- }
- case VAL_BOOL: {
- if (wireType === WT_VARINT) {
- const [val, np2] = decodeVarint(buf, pos);
- pos = np2;
- return val !== 0n;
- }
- return false;
- }
- case VAL_STRUCT: {
- if (wireType === WT_LEN) {
- const [len, np2] = decodeVarint(buf, pos);
- pos = np2;
- const lenN = checkedLen(len, pos, buf);
- const inner = buf.subarray(pos, pos + lenN);
- pos += lenN;
- return decodeProtobufStruct(inner);
- }
- return {};
- }
- case VAL_LIST: {
- if (wireType === WT_LEN) {
- const [len, np2] = decodeVarint(buf, pos);
- pos = np2;
- const lenN = checkedLen(len, pos, buf);
- const inner = buf.subarray(pos, pos + lenN);
- pos += lenN;
- return decodeProtobufList(inner);
- }
- return [];
- }
- default:
- // Skip unknown field
- if (wireType === WT_VARINT) {
- [, pos] = decodeVarint(buf, pos);
- } else if (wireType === WT_LEN) {
- const [len, np2] = decodeVarint(buf, pos);
- pos = np2;
- pos += Number(len);
- } else if (wireType === 1) {
- pos += 8;
- } else if (wireType === 5) {
- pos += 4;
- }
- }
+ const decoder = PROTOBUF_VALUE_DECODERS[fieldNumber];
+ if (decoder) return decoder(buf, pos, wireType).value;
+ pos = skipUnknownProtobufField(buf, pos, wireType);
}
return null;
}
+function decodeProtobufStructEntry(payload: Buffer): { key: string; value: unknown } | null {
+ const fields = decodeFields(payload);
+ const key = findLengthDelimitedField(fields, MAP_KEY)?.toString("utf8") ?? "";
+ const valueBytes = findLengthDelimitedField(fields, MAP_VALUE);
+ return key && valueBytes ? { key, value: decodeProtobufValue(valueBytes) } : null;
+}
+
function decodeProtobufStruct(buf: Buffer): Record {
const result: Record = {};
- for (const f of decodeFields(buf)) {
- if (f.fieldNumber === STRUCT_FIELDS && f.wireType === 2) {
- let key = "";
- let valueBytes: Buffer | null = null;
- for (const entry of decodeFields(f.bytes)) {
- if (entry.fieldNumber === MAP_KEY && entry.wireType === 2) {
- key = entry.bytes.toString("utf8");
- } else if (entry.fieldNumber === MAP_VALUE && entry.wireType === 2) {
- valueBytes = entry.bytes;
- }
- }
- if (key && valueBytes) {
- result[key] = decodeProtobufValue(valueBytes);
- }
- }
+ for (const field of decodeFields(buf)) {
+ if (field.fieldNumber !== STRUCT_FIELDS || field.wireType !== WT_LEN) continue;
+ const entry = decodeProtobufStructEntry(field.bytes);
+ if (entry) result[entry.key] = entry.value;
}
return result;
}
@@ -1477,6 +1459,39 @@ export type ChatMessage = {
tool_call_id?: string;
};
+function messageContentToText(content: ChatMessage["content"]): string {
+ if (typeof content === "string") return content;
+ if (!Array.isArray(content)) return "";
+ return content
+ .map((part) => (typeof part?.text === "string" ? part.text : ""))
+ .filter(Boolean)
+ .join("\n");
+}
+
+function assistantMessageLines(message: ChatMessage, text: string): string[] {
+ const lines = text ? [`Assistant: ${text}`] : [];
+ for (const toolCall of message.tool_calls ?? []) {
+ const name = toolCall.function?.name ?? "(unknown)";
+ const args = toolCall.function?.arguments ?? "";
+ lines.push(`Assistant called tool ${name} (${toolCall.id}) with arguments: ${args}`);
+ }
+ return lines;
+}
+
+function chatMessageLines(message: ChatMessage): string[] {
+ const text = messageContentToText(message.content);
+ if (message.role === "user") return text ? [`User: ${text}`] : [];
+ if (message.role === "assistant") return assistantMessageLines(message, text);
+ if (message.role === "tool") {
+ return [`Tool result (${message.tool_call_id ?? "(unknown)"}): ${text}`];
+ }
+ return text ? [`${message.role}: ${text}`] : [];
+}
+
+function joinSystemText(systemTexts: string[], body: string): string {
+ return systemTexts.length > 0 ? `${systemTexts.join("\n\n")}\n\n${body}` : body;
+}
+
/**
* Flatten an OpenAI-shaped message list down to a single user-text string
* suitable for cursor's UserMessage. The agent endpoint expects ONE user
@@ -1490,57 +1505,23 @@ export type ChatMessage = {
export function flattenMessages(messages: ChatMessage[]): string {
if (!Array.isArray(messages) || messages.length === 0) return "";
- const partsToText = (content: ChatMessage["content"]): string => {
- if (typeof content === "string") return content;
- if (content == null) return "";
- if (!Array.isArray(content)) return "";
- return content
- .map((p) => (typeof p?.text === "string" ? p.text : ""))
- .filter(Boolean)
- .join("\n");
- };
-
// System instructions go first as a labeled prefix. (The cursor executor
// routes system messages through the KV blob channel — see Phase 7 — but
// this branch is kept for non-cursor callers.)
const systemTexts = messages
.filter((m) => m.role === "system")
- .map((m) => partsToText(m.content))
+ .map((m) => messageContentToText(m.content))
.filter(Boolean);
const turn = messages.filter((m) => m.role !== "system");
// Single-user-message fast path (no tool_calls, no labels).
if (turn.length === 1 && turn[0].role === "user" && !turn[0].tool_calls) {
- const userText = partsToText(turn[0].content);
- return systemTexts.length > 0 ? `${systemTexts.join("\n\n")}\n\n${userText}` : userText;
+ return joinSystemText(systemTexts, messageContentToText(turn[0].content));
}
// Multi-turn / tool-using format. Each message is labeled. Tool calls
// and tool results get their own labeled lines.
- const lines: string[] = [];
- for (const m of turn) {
- const text = partsToText(m.content);
- if (m.role === "user") {
- if (text) lines.push(`User: ${text}`);
- } else if (m.role === "assistant") {
- if (text) lines.push(`Assistant: ${text}`);
- if (Array.isArray(m.tool_calls)) {
- for (const tc of m.tool_calls) {
- const args = tc.function?.arguments ?? "";
- lines.push(
- `Assistant called tool ${tc.function?.name ?? "(unknown)"} ` +
- `(${tc.id}) with arguments: ${args}`
- );
- }
- }
- } else if (m.role === "tool") {
- const callId = m.tool_call_id ?? "(unknown)";
- lines.push(`Tool result (${callId}): ${text}`);
- } else {
- if (text) lines.push(`${m.role}: ${text}`);
- }
- }
- const labelled = lines.join("\n\n");
- return systemTexts.length > 0 ? `${systemTexts.join("\n\n")}\n\n${labelled}` : labelled;
+ const labelled = turn.flatMap(chatMessageLines).join("\n\n");
+ return joinSystemText(systemTexts, labelled);
}
diff --git a/open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts b/open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts
new file mode 100644
index 0000000000..da431746eb
--- /dev/null
+++ b/open-sse/utils/cursorAgentProtobuf/requestedModelParameters.ts
@@ -0,0 +1,113 @@
+export const CURSOR_EFFORT_SUFFIXES = ["low", "medium", "high", "xhigh", "max"] as const;
+
+type CursorRequestedModel = {
+ modelId: string;
+ parameters: Array<{ id: string; value: string }>;
+};
+
+const CURSOR_ONE_MILLION_SUFFIX = "-1m";
+const CURSOR_GPT_REASONING_LEVELS = ["none", ...CURSOR_EFFORT_SUFFIXES] as const;
+
+const CURSOR_CLAUDE_ONE_MILLION_FAMILIES = [
+ {
+ legacyPrefix: "claude-fable-5-1",
+ modelId: "claude-fable-5-1",
+ supportsFast: false,
+ trailingThinking: false,
+ },
+ {
+ legacyPrefix: "claude-opus-5",
+ modelId: "claude-opus-5",
+ supportsFast: true,
+ trailingThinking: false,
+ },
+ {
+ legacyPrefix: "claude-opus-4-8",
+ modelId: "claude-opus-4-8",
+ supportsFast: true,
+ trailingThinking: false,
+ },
+ {
+ legacyPrefix: "claude-sonnet-5",
+ modelId: "claude-sonnet-5",
+ supportsFast: false,
+ trailingThinking: false,
+ },
+ {
+ legacyPrefix: "claude-4.6-sonnet",
+ modelId: "claude-sonnet-4-6",
+ supportsFast: false,
+ trailingThinking: true,
+ },
+] as const;
+
+type CursorClaudeOneMillionFamily = (typeof CURSOR_CLAUDE_ONE_MILLION_FAMILIES)[number];
+
+function isCursorEffort(value: string): value is (typeof CURSOR_EFFORT_SUFFIXES)[number] {
+ return CURSOR_EFFORT_SUFFIXES.some((effort) => effort === value);
+}
+
+function resolveGptOneMillionContextModel(legacyId: string): CursorRequestedModel | null {
+ const match = /^(gpt-5\.6-(?:sol|terra|luna))-(none|low|medium|high|xhigh|max)$/.exec(legacyId);
+ if (!match) return null;
+
+ const [, modelId, reasoning] = match;
+ if (!CURSOR_GPT_REASONING_LEVELS.some((level) => level === reasoning)) return null;
+ return {
+ modelId,
+ parameters: [
+ { id: "context", value: "1m" },
+ { id: "reasoning", value: reasoning },
+ { id: "fast", value: "false" },
+ ],
+ };
+}
+
+function resolveClaudeOneMillionVariant(
+ legacyId: string,
+ family: CursorClaudeOneMillionFamily
+): CursorRequestedModel | null {
+ const prefix = `${family.legacyPrefix}-`;
+ if (!legacyId.startsWith(prefix)) return null;
+
+ let variant = legacyId.slice(prefix.length);
+ const fast = variant.endsWith("-fast");
+ if (fast) variant = variant.slice(0, -"-fast".length);
+ if (fast && !family.supportsFast) return null;
+
+ const trailingThinking = family.trailingThinking && variant.endsWith("-thinking");
+ const leadingThinking = !family.trailingThinking && variant.startsWith("thinking-");
+ if (trailingThinking) variant = variant.slice(0, -"-thinking".length);
+ if (leadingThinking) variant = variant.slice("thinking-".length);
+ if (!isCursorEffort(variant)) return null;
+
+ const parameters = [
+ { id: "thinking", value: String(trailingThinking || leadingThinking) },
+ { id: "context", value: "1m" },
+ { id: "effort", value: variant },
+ ];
+ if (family.supportsFast) parameters.push({ id: "fast", value: String(fast) });
+ return { modelId: family.modelId, parameters };
+}
+
+function resolveClaudeOneMillionContextModel(legacyId: string): CursorRequestedModel | null {
+ for (const family of CURSOR_CLAUDE_ONE_MILLION_FAMILIES) {
+ const resolved = resolveClaudeOneMillionVariant(legacyId, family);
+ if (resolved) return resolved;
+ }
+ return null;
+}
+
+/**
+ * Cursor reuses each legacy slug for both its default and 1M context variants,
+ * so the public catalog adds a terminal `-1m` discriminator. Translate that
+ * synthetic id to the canonical wire model plus the complete parameter set
+ * reported by Cursor's AvailableModels metadata.
+ */
+export function resolveOneMillionContextModel(normalized: string): CursorRequestedModel | null {
+ if (!normalized.endsWith(CURSOR_ONE_MILLION_SUFFIX)) return null;
+ const legacyId = normalized.slice(0, -CURSOR_ONE_MILLION_SUFFIX.length);
+ return (
+ resolveGptOneMillionContextModel(legacyId) ?? resolveClaudeOneMillionContextModel(legacyId)
+ );
+}
diff --git a/open-sse/utils/reasoningFields.ts b/open-sse/utils/reasoningFields.ts
index 21fc22cab1..75b7cbe537 100644
--- a/open-sse/utils/reasoningFields.ts
+++ b/open-sse/utils/reasoningFields.ts
@@ -21,45 +21,61 @@ export function extractReasoningDetailsText(value: unknown): string {
.join("");
}
-export function getReadableReasoningValue(value: unknown): string {
+/**
+ * Consolidated reasoning field extraction - single pass returns all categories
+ * to avoid 3-5 separate object traversals per chunk.
+ */
+export interface ReasoningFields {
+ readable: string;
+ unsupported: string;
+ any: string;
+ hasUnsupportedSignal: boolean;
+ hasAnySignal: boolean;
+}
+
+export function extractReasoningFields(value: unknown): ReasoningFields {
const record = asReasoningRecord(value);
- return nonEmptyString(record.reasoning_content) || nonEmptyString(record.reasoning);
+
+ const readable = nonEmptyString(record.reasoning_content) || nonEmptyString(record.reasoning);
+ const reasoningText = nonEmptyString(record.reasoning_text);
+ const thinking = nonEmptyString(record.thinking);
+ const thought = nonEmptyString(record.thought);
+ const details = extractReasoningDetailsText(record);
+
+ const unsupported = reasoningText || thinking || thought || details;
+ const any = readable || unsupported;
+
+ const hasUnsupportedSignal = !!(
+ !readable &&
+ (reasoningText ||
+ thinking ||
+ thought ||
+ (Array.isArray(record.reasoning_details) && record.reasoning_details.length > 0))
+ );
+ const hasAnySignal = !!any;
+
+ return { readable, unsupported, any, hasUnsupportedSignal, hasAnySignal };
+}
+
+/** Back-compat wrappers for existing callers - delegate to consolidated extractor. */
+export function getReadableReasoningValue(value: unknown): string {
+ return extractReasoningFields(value).readable;
}
export function getUnsupportedReasoningValue(value: unknown): string {
- const record = asReasoningRecord(value);
- return (
- nonEmptyString(record.reasoning_text) ||
- nonEmptyString(record.thinking) ||
- nonEmptyString(record.thought) ||
- extractReasoningDetailsText(record)
- );
+ return extractReasoningFields(value).unsupported;
}
export function getAnyReasoningValue(value: unknown): string {
- return getReadableReasoningValue(value) || getUnsupportedReasoningValue(value);
+ return extractReasoningFields(value).any;
}
export function hasUnsupportedReasoningSignal(value: unknown): boolean {
- const record = asReasoningRecord(value);
- return Boolean(
- !getReadableReasoningValue(record) &&
- (nonEmptyString(record.reasoning_text) ||
- nonEmptyString(record.thinking) ||
- nonEmptyString(record.thought) ||
- (Array.isArray(record.reasoning_details) && record.reasoning_details.length > 0))
- );
+ return extractReasoningFields(value).hasUnsupportedSignal;
}
export function hasAnyReasoningSignal(value: unknown): boolean {
- const record = asReasoningRecord(value);
- return Boolean(
- getReadableReasoningValue(record) ||
- nonEmptyString(record.reasoning_text) ||
- nonEmptyString(record.thinking) ||
- nonEmptyString(record.thought) ||
- (Array.isArray(record.reasoning_details) && record.reasoning_details.length > 0)
- );
+ return extractReasoningFields(value).hasAnySignal;
}
const STRIPPABLE_REASONING_FIELDS = [
diff --git a/open-sse/utils/registeredEffortVariants.ts b/open-sse/utils/registeredEffortVariants.ts
index 06e2d5dfc6..2cef8d6721 100644
--- a/open-sse/utils/registeredEffortVariants.ts
+++ b/open-sse/utils/registeredEffortVariants.ts
@@ -14,10 +14,9 @@ export function getRegisteredProviderEffortBaseModelId(
modelId: string
): string | null {
const providerModels = getProviderModels(providerId);
+ const registeredVariant = providerModels.find((candidate) => candidate.id === modelId);
- if (!providerModels.some((candidate) => candidate.id === modelId)) {
- return null;
- }
+ if (!registeredVariant) return null;
for (const effort of REGISTERED_EFFORT_SUFFIXES) {
const suffix = `-${effort}`;
@@ -25,7 +24,15 @@ export function getRegisteredProviderEffortBaseModelId(
const baseModelId = modelId.slice(0, -suffix.length);
- return providerModels.some((candidate) => candidate.id === baseModelId) ? baseModelId : null;
+ if (providerModels.some((candidate) => candidate.id === baseModelId)) return baseModelId;
+
+ // Curated providers may intentionally expose only useful variants while the
+ // authoritative live catalog exposes their unsuffixed wire model. The registry
+ // declaration is the proof; never infer this relationship from spelling alone.
+ const declaredLiveBase = registeredVariant.liveCatalogIds?.find(
+ (candidate) => candidate === baseModelId || !candidate.endsWith(`-${effort}`)
+ );
+ return declaredLiveBase ?? null;
}
return null;
diff --git a/open-sse/utils/resourcePressure.ts b/open-sse/utils/resourcePressure.ts
index acef067a1e..f3ba7f77d9 100644
--- a/open-sse/utils/resourcePressure.ts
+++ b/open-sse/utils/resourcePressure.ts
@@ -67,9 +67,47 @@ function requireDuration(name: string, value: number): number {
return value;
}
-function buildCriticalGuard(reason: PressureReason): ResourcePressureGuardResult {
+/**
+ * Human-readable key=value detail appended to the rejection log line. Every
+ * rejection (immediate heap trip AND cached-critical-state reuse) goes
+ * through here, so this is the one place that needs the actual numbers —
+ * the bare reason code alone ("psi_some") gives an operator nothing to act
+ * on when deciding whether the guard is mistuned vs. genuinely saturated.
+ */
+function formatPressureDetail(detail: Record): string {
+ return Object.entries(detail)
+ .filter(([, value]) => value !== undefined)
+ .map(([key, value]) => `${key}=${value ?? "null"}`)
+ .join(" ");
+}
+
+/** Builds buildCriticalGuard's detail object for the cached-critical-state
+ * reuse path in check() -- pulled out of check() itself so that function's
+ * own cyclomatic complexity stays under the ratchet, not because this needs
+ * to be reused anywhere else. */
+function describeCachedPressure(params: {
+ signals: ResourceSignals | null;
+ recoveryStreak: number;
+ cacheAgeMs: number;
+}): Record {
+ const cgroup = params.signals?.cgroup;
+ return {
+ psiSomeAvg10: params.signals?.psi?.someAvg10 ?? null,
+ psiFullAvg10: params.signals?.psi?.fullAvg10 ?? null,
+ cgroupCurrentMb: cgroup?.currentBytes ? Math.round(cgroup.currentBytes / MB) : null,
+ cgroupMaxMb: cgroup?.maxBytes ? Math.round(cgroup.maxBytes / MB) : null,
+ recoveryStreak: params.recoveryStreak,
+ sampleAgeMs: params.cacheAgeMs,
+ };
+}
+
+function buildCriticalGuard(
+ reason: PressureReason,
+ detail: Record = {}
+): ResourcePressureGuardResult {
+ const detailText = formatPressureDetail(detail);
console.warn(
- `[resourcePressure] critical pressure guard tripped (reason=${reason}); returning 503`
+ `[resourcePressure] critical pressure guard tripped (reason=${reason}${detailText ? " " + detailText : ""}); returning 503`
);
return {
success: false,
@@ -97,7 +135,10 @@ function immediateHeapGuard(
if (thresholdMb == null) return null;
const guard = checkHeapPressureGuard(heapUsedMb, thresholdMb);
if (!guard) return null;
- return buildCriticalGuard("v8_heap_absolute");
+ return buildCriticalGuard("v8_heap_absolute", {
+ heapUsedMb: Math.round(heapUsedMb),
+ thresholdMb: Math.round(thresholdMb),
+ });
}
export function createResourcePressureRuntime(
@@ -192,9 +233,17 @@ export function createResourcePressureRuntime(
return immediate;
}
const cacheAge = lastSignals ? Math.max(0, now - lastRefreshAtMs) : Number.POSITIVE_INFINITY;
- return cacheAge <= maxStaleMs && state.severity === "critical"
- ? buildCriticalGuard(state.reason)
- : null;
+ if (cacheAge > maxStaleMs || state.severity !== "critical") {
+ return null;
+ }
+ return buildCriticalGuard(
+ state.reason,
+ describeCachedPressure({
+ signals: lastSignals,
+ recoveryStreak: state.recoveryStreak,
+ cacheAgeMs: cacheAge,
+ })
+ );
},
getObservation: () => ({ signals: lastSignals, state }),
whenRefreshSettled: async () => {
diff --git a/open-sse/utils/resourcePressurePolicy.ts b/open-sse/utils/resourcePressurePolicy.ts
index 447a084b61..3887bf9bbc 100644
--- a/open-sse/utils/resourcePressurePolicy.ts
+++ b/open-sse/utils/resourcePressurePolicy.ts
@@ -74,12 +74,30 @@ export const DEFAULT_RESOURCE_PRESSURE_THRESHOLDS: ResourcePressureThresholds =
highRatio: 0.85,
criticalRatio: 0.92,
recoveryRatio: 0.75,
- highPsiAvg10: 20,
- criticalPsiAvg10: 40,
- recoveryPsiAvg10: 10,
+ // Bumped 50% (20/40/10 -> 30/60/15): /proc/pressure/memory reflects
+ // HOST-wide PSI, not this process's own cgroup pressure (confirmed by
+ // comparing /proc/pressure/memory against /sys/fs/cgroup/memory.pressure
+ // from inside a running container -- the two differ). On a shared host
+ // running many unrelated workloads, host-wide memory contention from
+ // OTHER processes was tripping this guard even while OmniRoute's own
+ // usage stayed trivial. The ratio-based thresholds above stay untouched
+ // -- they're this process's own real OOM safety margin and unaffected by
+ // noisy neighbors.
+ highPsiAvg10: 30,
+ criticalPsiAvg10: 60,
+ recoveryPsiAvg10: 15,
sustainedSamplesHigh: 2,
sustainedSamplesCritical: 2,
- sustainedSamplesRecovery: 3,
+ // PSI's own avg10 is a kernel-computed 10s rolling average, so it already
+ // lags real recovery by design -- requiring 3 consecutive samples *on top*
+ // of that (at the ~1s default sample cadence) stacked another ~2-3s of
+ // guard-still-shedding time after the process was actually fine again.
+ // isRecovered() already requires every tracked ratio/PSI value to clear
+ // the separate, more conservative recoveryRatio/recoveryPsiAvg10
+ // thresholds (not just dip under the critical ones), so a single clean
+ // sample is real signal, not noise -- the streak requirement was adding
+ // redundant delay on top of an already-conservative bar.
+ sustainedSamplesRecovery: 1,
heapAbsoluteThresholdMb: null,
};
diff --git a/open-sse/utils/responsesStreamHelpers.ts b/open-sse/utils/responsesStreamHelpers.ts
index a2cba80fc1..f77b2a0c53 100644
--- a/open-sse/utils/responsesStreamHelpers.ts
+++ b/open-sse/utils/responsesStreamHelpers.ts
@@ -98,25 +98,29 @@ function buildResponsesOutputItemKey(item: unknown): string | null {
return `${type}:${id}:${callId}:${outputIndex}:${name}`;
}
+// Module-level Set reused across calls to avoid allocation per event
+const _seenResponsesKeys = new Set();
+
export function pushUniqueResponsesOutputItems(target: unknown[], items: readonly unknown[]) {
- const seen = new Set();
+ // Clear the reused Set instead of allocating new one
+ _seenResponsesKeys.clear();
for (const existingItem of target) {
const key = buildResponsesOutputItemKey(existingItem);
if (key) {
- seen.add(key);
+ _seenResponsesKeys.add(key);
}
}
for (const item of items) {
const key = buildResponsesOutputItemKey(item);
- if (key && seen.has(key)) {
+ if (key && _seenResponsesKeys.has(key)) {
continue;
}
target.push(item);
if (key) {
- seen.add(key);
+ _seenResponsesKeys.add(key);
}
}
}
diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts
index dd37eda217..d33cc8a526 100644
--- a/open-sse/utils/stream.ts
+++ b/open-sse/utils/stream.ts
@@ -178,6 +178,8 @@ type StreamOptions = {
* codex-compatible `namespace` + `name` fields.
*/
requestToolIdentityMap?: Map | null;
+ /** High water mark for the TransformStream internal buffer (default: 16384) */
+ highWaterMark?: number;
};
type TranslateState = ReturnType & {
@@ -1173,6 +1175,8 @@ export function createSSEStream(options: StreamOptions = {}) {
}
};
+ const highWaterMark = options.highWaterMark ?? 16384;
+
return new TransformStream(
{
start(controller) {
@@ -2992,8 +2996,8 @@ export function createSSEStream(options: StreamOptions = {}) {
clearIdleTimer();
},
},
- { highWaterMark: 16384 },
- { highWaterMark: 16384 }
+ { highWaterMark },
+ { highWaterMark }
);
}
@@ -3015,7 +3019,8 @@ export function createSSETransformStreamWithLogger(
copilotCompatibleReasoning = false,
suppressThinkClose = false,
customToolNames: ReadonlySet = new Set(),
- requestToolIdentityMap: Map | null = null
+ requestToolIdentityMap: Map | null = null,
+ highWaterMark?: number
) {
return createSSEStream({
mode: STREAM_MODE.TRANSLATE,
@@ -3034,6 +3039,7 @@ export function createSSETransformStreamWithLogger(
suppressThinkClose,
customToolNames,
requestToolIdentityMap,
+ highWaterMark,
});
}
@@ -3048,7 +3054,8 @@ export function createPassthroughStreamWithLogger(
apiKeyInfo: unknown = null,
onFailure: ((payload: StreamFailurePayload) => boolean | void | Promise) | null = null,
clientResponseFormat: string | null = null,
- requestToolIdentityMap: Map | null = null
+ requestToolIdentityMap: Map | null = null,
+ highWaterMark?: number
) {
return createSSEStream({
mode: STREAM_MODE.PASSTHROUGH,
@@ -3063,6 +3070,7 @@ export function createPassthroughStreamWithLogger(
onFailure,
clientResponseFormat,
requestToolIdentityMap,
+ highWaterMark,
});
}
diff --git a/open-sse/utils/streamHandler.ts b/open-sse/utils/streamHandler.ts
index dbcc439eef..7776f2e5e9 100644
--- a/open-sse/utils/streamHandler.ts
+++ b/open-sse/utils/streamHandler.ts
@@ -629,7 +629,11 @@ function resolveSilentCloseOutcome(input: {
return null;
}
-export function createDisconnectAwareStream(transformStream, streamController) {
+export function createDisconnectAwareStream(
+ transformStream,
+ streamController,
+ options: { highWaterMark?: number } = {}
+) {
const reader = transformStream.readable.getReader();
const writer = transformStream.writable.getWriter();
const terminalDecoder = new TextDecoder();
@@ -697,6 +701,8 @@ export function createDisconnectAwareStream(transformStream, streamController) {
}
};
+ const highWaterMark = options.highWaterMark ?? 16384;
+
return new ReadableStream(
{
async pull(controller) {
@@ -818,7 +824,7 @@ export function createDisconnectAwareStream(transformStream, streamController) {
await Promise.allSettled([reader.cancel(reason), writer.abort(reason)]);
},
},
- { highWaterMark: 16384 }
+ { highWaterMark }
);
}
@@ -845,7 +851,7 @@ export function pipeWithDisconnect(
providerResponse: Response,
transformStream: TransformStream,
streamController: StreamController,
- opts: { stallTimeoutMs?: number } = {}
+ opts: { stallTimeoutMs?: number; highWaterMark?: number } = {}
) {
const stallTimeoutMs = opts.stallTimeoutMs ?? DEFAULT_STREAM_STALL_TIMEOUT_MS;
@@ -854,7 +860,8 @@ export function pipeWithDisconnect(
const transformedBody = providerResponse.body.pipeThrough(transformStream);
return createDisconnectAwareStream(
{ readable: transformedBody, writable: createNoopAbortWritable() },
- streamController
+ streamController,
+ { highWaterMark: opts.highWaterMark }
);
}
@@ -956,6 +963,7 @@ export function pipeWithDisconnect(
.pipeThrough(transformStream);
return createDisconnectAwareStream(
{ readable: transformedBody, writable: createNoopAbortWritable() },
- wrappedController
+ wrappedController,
+ { highWaterMark: opts.highWaterMark }
);
}
diff --git a/open-sse/utils/streamHelpers.ts b/open-sse/utils/streamHelpers.ts
index db8c656d1d..39aafbacf9 100644
--- a/open-sse/utils/streamHelpers.ts
+++ b/open-sse/utils/streamHelpers.ts
@@ -70,6 +70,13 @@ function isRecord(value: unknown): value is Record {
const ANSI_ESCAPE_RE =
/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[A-Z\[\]\\^_`])|[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
+// Pre-compiled regex constants for hot-path SSE processing (avoid per-call compilation)
+const CR_STRIP_RE = /\r$/;
+const SSE_FIELD_RE = /^(?:event:|id:|retry:|:)/i;
+const SSE_EVENT_RE = /^event:\s*(.+)$/i;
+const SSE_ID_RETRY_RE = /^(?::|id:|retry:)/i;
+const SSE_EVENT_ONLY_RE = /^event:/i;
+
/**
* Strip ANSI/VT100 escape sequences (and stray C0 controls) from a string.
* Non-string inputs (null/undefined) are returned unchanged. Preserves \t \n \r.
@@ -125,7 +132,7 @@ export function parseSSELine(line: string): SSEJsonPayload | null {
}
function extractSseDataLine(line: string): string | null {
- const trimmed = stripAnsiCodes(line.trimStart().replace(/\r$/, ""));
+ const trimmed = stripAnsiCodes(line.trimStart().replace(CR_STRIP_RE, ""));
if (!trimmed.startsWith("data:")) return null;
return trimmed.slice(5).trimStart();
}
@@ -192,12 +199,12 @@ export function createSSEDataLineNormalizer(): SSEDataLineNormalizer {
normalize(lines: string[]) {
const output: string[] = [];
for (const line of lines) {
- const normalizedLine = line.replace(/\r$/, "");
+ const normalizedLine = line.replace(CR_STRIP_RE, "");
const trimmed = normalizedLine.trim();
if (
trimmed &&
- /^(?:event:|id:|retry:|:)/i.test(trimmed) &&
+ SSE_FIELD_RE.test(trimmed) &&
hasSelfDescribingPendingDataPayload()
) {
flush(output);
@@ -235,7 +242,7 @@ export function createSSEEventPrefixBuffer(options?: { forwardEvent?: boolean })
},
eventType() {
for (let i = lines.length - 1; i >= 0; i--) {
- const match = lines[i].trim().match(/^event:\s*(.+)$/i);
+ const match = lines[i].trim().match(SSE_EVENT_RE);
if (match) return match[1].trim();
}
return "";
@@ -251,10 +258,10 @@ export function createSSEEventPrefixBuffer(options?: { forwardEvent?: boolean })
// `id:`/`retry:` and bare `:` comment lines are not part of any of the
// OpenAI Chat-Completions, OpenAI Responses, or Claude Messages SSE
// protocols — never buffer (and thus never re-forward) them (#10017).
- if (/^(?::|id:|retry:)/i.test(trimmed)) return;
+ if (SSE_ID_RETRY_RE.test(trimmed)) return;
// `event:` framing is only forwarded for protocols that define it; drop it
// for plain OpenAI Chat-Completions-format clients.
- if (/^event:/i.test(trimmed) && !forwardEvent) return;
+ if (SSE_EVENT_ONLY_RE.test(trimmed) && !forwardEvent) return;
lines.push(line);
emitted = false;
},
diff --git a/open-sse/utils/streamPayloadCollector.ts b/open-sse/utils/streamPayloadCollector.ts
index c7aab45b7a..f0353ce0f5 100644
--- a/open-sse/utils/streamPayloadCollector.ts
+++ b/open-sse/utils/streamPayloadCollector.ts
@@ -885,8 +885,26 @@ export function compactStructuredStreamPayload(payload: unknown): unknown {
};
}
+// Live incident (2026-09-02): a reasoning-heavy response streams reasoning
+// token-by-token as hundreds to thousands of tiny SSE deltas BEFORE the real
+// output/tool_calls ever arrive. At the old defaults (200 events / 48KB) the
+// cap was routinely exhausted during the reasoning phase alone, dropping the
+// completion event entirely -- measured live: ~22% of a sample of recent
+// successful responses hit this. For a caller with no `format` (no live
+// reducer -- see the CollectorOptions.format doc comment), the logged
+// summary is reconstructed from getEvents() (open-sse/utils/stream.ts), so a
+// dropped completion event produced a served-successfully response logged
+// with status "in_progress" and empty output -- which
+// src/lib/db/responsesContinuationStore.ts then had nothing real to
+// reconstruct a later continuation turn from (see its own fail-closed fix,
+// 2026-09-02). Raising the cap doesn't eliminate the class of bug for an
+// arbitrarily long stream, but it removes it as a routine, everyday failure;
+// the format-driven live reducer (used by providerPayloadCollector, an
+// analogous prior fix) is the cap-independent fix and remains the deeper
+// follow-up for a caller that still wants build()'s summary correct beyond
+// any fixed cap.
export function createStructuredSSECollector(options: CollectorOptions = {}) {
- const { maxEvents = 200, maxBytes = 49152, stage, format, fallbackModel } = options;
+ const { maxEvents = 2000, maxBytes = 524288, stage, format, fallbackModel } = options;
const events: StructuredSSEEvent[] = [];
let usedBytes = 0;
let droppedEvents = 0;
diff --git a/open-sse/utils/usageTracking.ts b/open-sse/utils/usageTracking.ts
index 1b605acc0c..25a1a20d8b 100644
--- a/open-sse/utils/usageTracking.ts
+++ b/open-sse/utils/usageTracking.ts
@@ -680,10 +680,29 @@ export function isEmptyUsage(usage: unknown): boolean {
/**
* Extract usage from supported formats (Claude, OpenAI, Gemini, Responses API)
+ * Fast-path: return early for chunks without any usage-related fields.
+ * Most streaming chunks (content deltas) have no usage — avoids property checks.
*/
export function extractUsage(chunk: UsagePayloadLike | null | undefined) {
if (!chunk || typeof chunk !== "object") return null;
+ // Fast-path: check for any usage-like fields before doing full extraction
+ // Most chunks are content deltas with no usage — return null immediately.
+ const c = chunk as Record;
+ const response = c.response as Record | undefined;
+ const message = c.message as Record | undefined;
+ if (
+ !c.type &&
+ c.usage === undefined &&
+ c.usageMetadata === undefined &&
+ response?.usage === undefined &&
+ response?.usageMetadata === undefined &&
+ message?.usage === undefined &&
+ c.done !== true
+ ) {
+ return null;
+ }
+
// Claude/Antigravity streaming: message_start event carries INPUT tokens
// FIX #74: This event was not handled — input_tokens were being dropped
// Structure: { type: "message_start", message: { usage: { input_tokens: N, output_tokens: 0 } } }
diff --git a/package-lock.json b/package-lock.json
index 270593d514..6f720491df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54,7 +54,7 @@
"lucide-react": "^1.33.0",
"marked": "^18.0.11",
"marked-terminal": "^7.3.0",
- "material-symbols": "^0.46.0",
+ "material-symbols": "^0.47.0",
"mermaid": "^11.17.2",
"monaco-editor": "^0.56.0",
"next": "16.3.3",
@@ -62,7 +62,7 @@
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"omniglyph": "^1.4.0",
- "open": "^11.0.1",
+ "open": "^11.0.2",
"ora": "^9.4.1",
"parse5": "^8.0.1",
"pino": "^10.3.1",
@@ -85,7 +85,7 @@
"sql.js": "^1.14.2",
"tailwind-merge": "^3.6.0",
"tiktoken": "^1.0.22",
- "tsx": "^4.23.12",
+ "tsx": "^4.23.13",
"turndown": "7.2.4",
"turndown-plugin-gfm": "1.0.2",
"undici": "^8.10.0",
@@ -94,7 +94,7 @@
"ws": "^8.21.3",
"xxhash-wasm": "^1.1.0",
"yazl": "^3.3.1",
- "zod": "^4.4.3",
+ "zod": "^4.5.4",
"zustand": "^5.0.15"
},
"bin": {
@@ -148,7 +148,7 @@
"lint-staged": "^17.4.1",
"lockfile-lint": "^5.0.1",
"node-loader": "^2.1.0",
- "opencode-ai": "1.18.23",
+ "opencode-ai": "1.18.25",
"playwright-ctrf-json-reporter": "^0.0.29",
"prettier": "^3.9.6",
"promptfoo": "^0.122.1",
@@ -4578,29 +4578,43 @@
}
},
"node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true,
"license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
- "version": "0.16.7",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
- "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@humanfs/core": "^0.19.1",
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -14803,9 +14817,9 @@
}
},
"node_modules/@vitejs/plugin-react": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz",
- "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
+ "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15109,9 +15123,9 @@
}
},
"node_modules/@xmldom/xmldom": {
- "version": "0.9.10",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
- "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
+ "version": "0.9.12",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz",
+ "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -16252,9 +16266,9 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.13",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
- "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
+ "version": "2.11.20",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
+ "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -16612,9 +16626,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
- "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "version": "4.28.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+ "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
"dev": true,
"funding": [
{
@@ -16632,11 +16646,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.10.12",
- "caniuse-lite": "^1.0.30001782",
- "electron-to-chromium": "^1.5.328",
- "node-releases": "^2.0.36",
- "update-browserslist-db": "^1.2.3"
+ "baseline-browser-mapping": "^2.11.12",
+ "caniuse-lite": "^1.0.30001809",
+ "electron-to-chromium": "^1.5.402",
+ "node-releases": "^2.0.53",
+ "update-browserslist-db": "^1.3.0"
},
"bin": {
"browserslist": "cli.js"
@@ -17086,9 +17100,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001784",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz",
- "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==",
+ "version": "1.0.30001810",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"funding": [
{
"type": "opencollective",
@@ -19413,7 +19427,9 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
"integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+ "dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"bundle-name": "^4.1.0",
"default-browser-id": "^5.0.0"
@@ -20004,9 +20020,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.375",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
- "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
+ "version": "1.5.420",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
+ "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==",
"dev": true,
"license": "ISC"
},
@@ -21686,9 +21702,9 @@
}
},
"node_modules/fast-uri": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
- "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"funding": [
{
"type": "github",
@@ -28336,9 +28352,9 @@
}
},
"node_modules/material-symbols": {
- "version": "0.46.0",
- "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.46.0.tgz",
- "integrity": "sha512-YxmTXwOhLOI6EupAwFfxFERbaDe61dG/tveOSy2HecndGKqvJ74WqXrrXLNWpIGDkk6TDpieuQPDS+hA7+z3Ig==",
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.47.0.tgz",
+ "integrity": "sha512-/Wt7QSv5Hih8EFj9ySHnnF+NAGajTMRLkfajj0j4MNCY3FB2QhGRg5xtnBs5bUE63mzVo7J5XbbIDr0HeITpHg==",
"license": "Apache-2.0"
},
"node_modules/math-intrinsics": {
@@ -30762,9 +30778,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.47",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
- "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "version": "2.0.54",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
+ "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -31385,16 +31401,16 @@
"optional": true
},
"node_modules/open": {
- "version": "11.0.1",
- "resolved": "https://registry.npmjs.org/open/-/open-11.0.1.tgz",
- "integrity": "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw==",
+ "version": "11.0.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-11.0.2.tgz",
+ "integrity": "sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==",
"license": "MIT",
"dependencies": {
- "default-browser": "^5.4.0",
+ "default-browser": "^5.5.1",
"define-lazy-prop": "^3.0.0",
"is-in-ssh": "^1.0.0",
"is-inside-container": "^1.0.0",
- "powershell-utils": "^0.2.0",
+ "powershell-utils": "^0.2.1",
"wsl-utils": "^1.0.0"
},
"engines": {
@@ -31404,6 +31420,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/open/node_modules/default-browser": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz",
+ "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==",
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/openai": {
"version": "6.46.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.46.0.tgz",
@@ -31437,9 +31469,9 @@
}
},
"node_modules/opencode-ai": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.18.23.tgz",
- "integrity": "sha512-3NkT0XINL7d0HYkTyGV1SPChHXhvRgKqNaTgKRTGb0TXUWszXA7MW/y3zMZw29y1AQuUDAzRvVYmQ9KGRQhroA==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.18.25.tgz",
+ "integrity": "sha512-pS4RKJ9eKwU7Dp5G5pdj1rhMnpG5APixXzfTKNoFqv9aFVI36Rnza2jESvKifxyPZlsA65MQB03WCArY0EK6mg==",
"cpu": [
"arm64",
"x64"
@@ -31456,24 +31488,24 @@
"opencode": "bin/opencode.exe"
},
"optionalDependencies": {
- "opencode-darwin-arm64": "1.18.23",
- "opencode-darwin-x64": "1.18.23",
- "opencode-darwin-x64-baseline": "1.18.23",
- "opencode-linux-arm64": "1.18.23",
- "opencode-linux-arm64-musl": "1.18.23",
- "opencode-linux-x64": "1.18.23",
- "opencode-linux-x64-baseline": "1.18.23",
- "opencode-linux-x64-baseline-musl": "1.18.23",
- "opencode-linux-x64-musl": "1.18.23",
- "opencode-windows-arm64": "1.18.23",
- "opencode-windows-x64": "1.18.23",
- "opencode-windows-x64-baseline": "1.18.23"
+ "opencode-darwin-arm64": "1.18.25",
+ "opencode-darwin-x64": "1.18.25",
+ "opencode-darwin-x64-baseline": "1.18.25",
+ "opencode-linux-arm64": "1.18.25",
+ "opencode-linux-arm64-musl": "1.18.25",
+ "opencode-linux-x64": "1.18.25",
+ "opencode-linux-x64-baseline": "1.18.25",
+ "opencode-linux-x64-baseline-musl": "1.18.25",
+ "opencode-linux-x64-musl": "1.18.25",
+ "opencode-windows-arm64": "1.18.25",
+ "opencode-windows-x64": "1.18.25",
+ "opencode-windows-x64-baseline": "1.18.25"
}
},
"node_modules/opencode-darwin-arm64": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.18.23.tgz",
- "integrity": "sha512-QP9PjwpHtZoLVXw2WvUmPZecz7mWbQkT4t3K36B//fCaDG+zWa+SsztIeaW5azujNwwtUemLA5icE/zINng48Q==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.18.25.tgz",
+ "integrity": "sha512-W4dyMFtHBglWZ1SEooh3Ke9v1M9lv945Y58atb8e1yKII8YykJ8LknOFyKipYC028oPDO4IZc3GYGKbg9PCg2w==",
"cpu": [
"arm64"
],
@@ -31484,9 +31516,9 @@
]
},
"node_modules/opencode-darwin-x64": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.18.23.tgz",
- "integrity": "sha512-R9nWP3edz/0FnEfwmuxtiWBB7bS4NtZCyCffJyiMlrbwdDC+bIXYrWxWXVrzaP1mJujs6g2MAwTCUSx/qpBhDw==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.18.25.tgz",
+ "integrity": "sha512-YYKrfeUSJhD7hZl+yNmayS51sDwxiE9o5XwrfgYSSie6sOyHFc9Ei13VBkVU6T+IJHhFhTahOFAwSDxggrAnGA==",
"cpu": [
"x64"
],
@@ -31497,9 +31529,9 @@
]
},
"node_modules/opencode-darwin-x64-baseline": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.18.23.tgz",
- "integrity": "sha512-QGx6I/nFYur7qJ/Nx2L3fC4XYQt44cyDsm7p8twNA+cdjGX3ndnPbMdAl5ikdZyAfSMUGYK8VWY2JMxv0rmfjw==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.18.25.tgz",
+ "integrity": "sha512-rRgTaoTeIN2diL1e1HGZ48Zh4ynMDEB1jYjD76LaFUVzMwakEY1i7NvG8e/rbjRMDkgXIr6TwzCtIMcMOpLQMA==",
"cpu": [
"x64"
],
@@ -31510,9 +31542,9 @@
]
},
"node_modules/opencode-linux-arm64": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.18.23.tgz",
- "integrity": "sha512-g1zDFhuE9FOYwjSGderlu69wfd4GQzS0xsDiIY11QUciuBmM6DrHqLvmhlLFsUVHYVnCPY1YeN1pq5ewE3x72Q==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.18.25.tgz",
+ "integrity": "sha512-PMvcpFpha3yAhaVCC0QbegHPxsEZ0FuQf+52PXvqQut1r3w1l1Pilor9tUA7TyCRa4UokACI90nTmKmtMnQBag==",
"cpu": [
"arm64"
],
@@ -31523,9 +31555,9 @@
]
},
"node_modules/opencode-linux-arm64-musl": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.18.23.tgz",
- "integrity": "sha512-VyDkzUJfJgkx9h9RhazTW9xeTgSXBmVFPblsbPGBW9tR612f6gjQxfOfu4cpHnQHK1qjuW9ClzLKHWqv3EcJTA==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.18.25.tgz",
+ "integrity": "sha512-IwIPKmNwIjLshlSgjoRLKFwxxiLpZ5Y0zjv6r456RQtJKK62IbYGXkCm3AiSZ/lqGsu3XF+xn/Xza29ivgpgcg==",
"cpu": [
"arm64"
],
@@ -31539,9 +31571,9 @@
]
},
"node_modules/opencode-linux-x64": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.23.tgz",
- "integrity": "sha512-5x9d1Cm/YtqzR6lAlNbgVprTQ3R3hx7qGTWCzm5l5u6lBNkhYTrhy2s8k25dxKKuxqZ9Kngqz9JYWvSVHy2Lmw==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.25.tgz",
+ "integrity": "sha512-bdRSJ6gbK/EnLNWxROOQYXFXiUeqeFxGz8DIO8LCqnii99A2OWFAyZ3Da5gpvfT1Yrp9/lYL55n/tM3ale5smg==",
"cpu": [
"x64"
],
@@ -31552,9 +31584,9 @@
]
},
"node_modules/opencode-linux-x64-baseline": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.18.23.tgz",
- "integrity": "sha512-yUhBOXfTQour2JCdAkwD3DDqSnyxB0grefwdPqEhYmJHIkYxfJIIzyy6V//pyouvkE0XMouFtiuZXw8S6Wo0iQ==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.18.25.tgz",
+ "integrity": "sha512-+b0w7XyHx0XPQWHBk2JymXbXnyZQ2PjIPuu4a4QJgSUqGuGz1L2flA3wgpZVAWFUhrEIr9DFhBk3AkKKNgMuRw==",
"cpu": [
"x64"
],
@@ -31565,9 +31597,9 @@
]
},
"node_modules/opencode-linux-x64-baseline-musl": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.18.23.tgz",
- "integrity": "sha512-c1DPxauhzAurlIBhJBr/rokDpc65l084T4qTl36gDDT9Xzc/Nk5Q5yMDaPm1DDI3WeHKDt11MDlxT5AjQW5gtw==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.18.25.tgz",
+ "integrity": "sha512-E2JUeOOSXPbG1cNOzxnqjqkd0a3+oFmwkbJe6bZ308CFgLWBFfVh0fF42HTCEqfK+yYbidpEkQuEkUgxq/11IA==",
"cpu": [
"x64"
],
@@ -31581,9 +31613,9 @@
]
},
"node_modules/opencode-linux-x64-musl": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.18.23.tgz",
- "integrity": "sha512-t/5mlnTBZKdZpqKHwdwxlWqGakntauvMSmXtyJc17M7XJRmZaaGHtNSaSefbbYFIL4agoQCXTvIkhhyxOvr7zQ==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.18.25.tgz",
+ "integrity": "sha512-W15qTNDz1fsTzs1SkE6bB/gpIDBF3rwDbewUKdbyXD3dVs6umyugOql1T4u9n/gqWa/Z/VDURbn39VsejeSdbQ==",
"cpu": [
"x64"
],
@@ -31597,9 +31629,9 @@
]
},
"node_modules/opencode-windows-arm64": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.18.23.tgz",
- "integrity": "sha512-QtJQcLU0yPz6on3jjks3f/EHgZuIDFw7FvAKu3wsHhL09NYDh7GczfRXDPRHa3NgqnU8dkB8p9mhqgaPRPogoQ==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.18.25.tgz",
+ "integrity": "sha512-GFp74pProoPwqktHMf+9wQ8fza1RvFt0RG0iRtTQnJ4VWVY62qEeVuJkH6ki9QXS270HXyqtxvF8AuHQzuVZlA==",
"cpu": [
"arm64"
],
@@ -31610,9 +31642,9 @@
]
},
"node_modules/opencode-windows-x64": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.18.23.tgz",
- "integrity": "sha512-mMaIITuXzkNfjdcYL8uZaZuMDjulFyH/UCq9bxblam2mUZf9uWisoi5J6CXFsS/mkN7CZfTAt6PttSp4n3PH4g==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.18.25.tgz",
+ "integrity": "sha512-xW5wtSxWYbI7DcmQWMlNWIiDBdMJON1vDiEmVWo88R9tT/PaahOhWKgp7FoWDqJKf89jS3ZIzkqnkU3F2dio7A==",
"cpu": [
"x64"
],
@@ -31623,9 +31655,9 @@
]
},
"node_modules/opencode-windows-x64-baseline": {
- "version": "1.18.23",
- "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.18.23.tgz",
- "integrity": "sha512-AqXsTKaPcDx3rrid5bLUwJbQ/3vr9rJ6fvOStIznTzwrbOgP8wy5G4jCoIzu6KB/WxGx/d1MrV4cGaJ73qnjBA==",
+ "version": "1.18.25",
+ "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.18.25.tgz",
+ "integrity": "sha512-/28bGRQwT+2JdGbtGaNr95tstgysiULEXtcvgNg7yLDxitqmSVgd8V8XRGS0UWDdfiWWMND9A9T5EAsbF1/xDQ==",
"cpu": [
"x64"
],
@@ -32966,9 +32998,9 @@
}
},
"node_modules/powershell-utils": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.0.tgz",
- "integrity": "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw==",
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
+ "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
"license": "MIT",
"engines": {
"node": ">=20"
@@ -34020,12 +34052,13 @@
}
},
"node_modules/qs": {
- "version": "6.15.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
- "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.1.0"
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
@@ -35725,14 +35758,14 @@
}
},
"node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@@ -35744,13 +35777,13 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -37629,9 +37662,9 @@
"license": "0BSD"
},
"node_modules/tsx": {
- "version": "4.23.12",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz",
- "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==",
+ "version": "4.23.13",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
+ "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
"license": "MIT",
"dependencies": {
"esbuild": "~0.28.0"
@@ -38338,9 +38371,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
+ "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"dev": true,
"funding": [
{
@@ -39902,9 +39935,9 @@
}
},
"node_modules/zod": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz",
+ "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index f019c45e90..4925d88658 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "omniroute",
"version": "3.8.51",
- "description": "Unified AI router with 355 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
+ "description": "Unified AI router with 356 providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
"type": "module",
"bin": {
"omniroute": "bin/omniroute.mjs",
@@ -95,6 +95,7 @@
"bench:compression": "bun scripts/compression/benchmark.ts",
"bench:heap-body": "node --expose-gc --import tsx/esm scripts/perf/request-body-heap.ts",
"bench:routing-events": "node --import tsx/esm scripts/perf/routing-events-bench.ts",
+ "bench:highwatermark": "node --import tsx/esm scripts/perf/benchmark-highwatermark.ts",
"eval:compression": "node --import tsx scripts/compression-eval/index.ts",
"eval:router": "node --import tsx scripts/router-eval/index.ts",
"eval:router:compare": "node --import tsx scripts/router-eval/compare.ts",
@@ -150,7 +151,8 @@
"check:router-eval": "node --import tsx scripts/check/check-router-eval-regression.ts",
"check:doc-links": "node scripts/check/check-doc-links.mjs",
"check:fabricated-docs": "node scripts/check/check-fabricated-docs.mjs --strict",
- "check:docs-all": "npm run check:docs-sync && npm run check:docs-counts && npm run check:env-doc-sync && npm run check:deprecated-versions && npm run check:doc-links && npm run check:fabricated-docs",
+ "check:docs-frontmatter": "node scripts/check/check-docs-frontmatter.mjs",
+ "check:docs-all": "npm run check:docs-sync && npm run check:docs-frontmatter && npm run check:docs-counts && npm run check:env-doc-sync && npm run check:deprecated-versions && npm run check:doc-links && npm run check:fabricated-docs",
"docs:render-diagrams": "node scripts/docs/render-diagrams.mjs",
"i18n:run": "node scripts/i18n/run-translation.mjs",
"i18n:run:dry": "node scripts/i18n/run-translation.mjs --dry-run",
@@ -319,7 +321,7 @@
"lucide-react": "^1.33.0",
"marked": "^18.0.11",
"marked-terminal": "^7.3.0",
- "material-symbols": "^0.46.0",
+ "material-symbols": "^0.47.0",
"mermaid": "^11.17.2",
"monaco-editor": "^0.56.0",
"next": "16.3.3",
@@ -327,7 +329,7 @@
"next-themes": "^0.4.6",
"node-machine-id": "^1.1.12",
"omniglyph": "^1.4.0",
- "open": "^11.0.1",
+ "open": "^11.0.2",
"ora": "^9.4.1",
"parse5": "^8.0.1",
"pino": "^10.3.1",
@@ -350,7 +352,7 @@
"sql.js": "^1.14.2",
"tailwind-merge": "^3.6.0",
"tiktoken": "^1.0.22",
- "tsx": "^4.23.12",
+ "tsx": "^4.23.13",
"turndown": "7.2.4",
"turndown-plugin-gfm": "1.0.2",
"undici": "^8.10.0",
@@ -359,7 +361,7 @@
"ws": "^8.21.3",
"xxhash-wasm": "^1.1.0",
"yazl": "^3.3.1",
- "zod": "^4.4.3",
+ "zod": "^4.5.4",
"zustand": "^5.0.15"
},
"optionalDependencies": {
@@ -419,7 +421,7 @@
"lint-staged": "^17.4.1",
"lockfile-lint": "^5.0.1",
"node-loader": "^2.1.0",
- "opencode-ai": "1.18.23",
+ "opencode-ai": "1.18.25",
"playwright-ctrf-json-reporter": "^0.0.29",
"prettier": "^3.9.6",
"promptfoo": "^0.122.1",
@@ -467,7 +469,7 @@
"sharp": "^0.35.4",
"postcss": "^8.5.18",
"ip-address": "^10.3.1",
- "qs": "^6.15.2",
+ "qs": "^6.16.0",
"uuid": "^14.0.2",
"form-data": "^4.0.6",
"vite": "^8.0.16",
@@ -475,7 +477,7 @@
"@babel/core": "^7.29.6",
"hono": "^4.12.34",
"@hono/node-server": "^2.0.5",
- "fast-uri": "^3.1.5",
+ "fast-uri": "^3.1.7",
"body-parser": "^2.3.0",
"@yarnpkg/parsers": {
"js-yaml": "^4.3.1"
diff --git a/public/images/tier-flow-dark.svg b/public/images/tier-flow-dark.svg
index 1cf2589812..8f7fcde48f 100644
--- a/public/images/tier-flow-dark.svg
+++ b/public/images/tier-flow-dark.svg
@@ -1,6 +1,6 @@
-
+
OmniRoute 4-tier fallback
- OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 355 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.
+ OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 356 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.
@@ -15,7 +15,7 @@
OmniRoute 4-tier fallback
- Never stop building — automatic zero-config failover across 355 providers
+ Never stop building — automatic zero-config failover across 356 providers
diff --git a/public/images/tier-flow-light.svg b/public/images/tier-flow-light.svg
index cd79d47e3b..5ad3a108f7 100644
--- a/public/images/tier-flow-light.svg
+++ b/public/images/tier-flow-light.svg
@@ -1,6 +1,6 @@
-
+
OmniRoute 4-tier fallback
- OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 355 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.
+ OmniRoute 4-tier fallback: your IDE or CLI calls one local endpoint and the OmniRoute Smart Router fails over across 356 providers in 4 tiers — Tier 1 Subscription, Tier 2 API, Tier 3 Cheap, Tier 4 Free.
@@ -15,7 +15,7 @@
OmniRoute 4-tier fallback
- Never stop building — automatic zero-config failover across 355 providers
+ Never stop building — automatic zero-config failover across 356 providers
diff --git a/scripts/check/check-docs-frontmatter.mjs b/scripts/check/check-docs-frontmatter.mjs
new file mode 100644
index 0000000000..a00751f880
--- /dev/null
+++ b/scripts/check/check-docs-frontmatter.mjs
@@ -0,0 +1,103 @@
+#!/usr/bin/env node
+/**
+ * Validates the frontmatter of every Markdown file that fumadocs-mdx compiles.
+ *
+ * Why this gate exists: `source.config.ts` feeds `docs/**` globs to
+ * `defineDocs()`, and fumadocs' default frontmatter schema REQUIRES a `title`
+ * string. A doc added without frontmatter does not fail any docs gate — it
+ * fails the **production build** with a generic Turbopack error
+ * (`[MDX] invalid frontmatter … title: Invalid input: expected string,
+ * received undefined`), which then cascades into `check:pack-artifact` and the
+ * tarball boot-smoke. That is exactly how #12478 turned the release branch red
+ * (base-red #12581): one new reference doc, no frontmatter, three failing
+ * gates and an unbuildable branch.
+ *
+ * Catching it here costs milliseconds instead of a full Next build.
+ *
+ * The globs are read from `source.config.ts` rather than duplicated, so adding
+ * a new docs directory there cannot silently escape this check.
+ */
+
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
+const CONFIG_PATH = path.join(ROOT, "source.config.ts");
+
+/** Extract the `files: [...]` globs declared in source.config.ts. */
+function readConfiguredGlobs() {
+ const src = fs.readFileSync(CONFIG_PATH, "utf-8");
+ const block = src.match(/files\s*:\s*\[([\s\S]*?)\]/);
+ if (!block) {
+ console.error(
+ "[docs-frontmatter] FAIL — could not locate the `files:` globs in source.config.ts"
+ );
+ process.exit(1);
+ }
+ const globs = [...block[1].matchAll(/["'`]([^"'`]+)["'`]/g)].map((m) => m[1]);
+ if (globs.length === 0) {
+ console.error("[docs-frontmatter] FAIL — source.config.ts declares no doc globs");
+ process.exit(1);
+ }
+ return globs;
+}
+
+/** "./reference/**\/*.md" -> the directory under docs/ it covers. */
+function globToDir(glob) {
+ const cleaned = glob.replace(/^\.\//, "");
+ const dir = cleaned.split("/**")[0];
+ return path.join(ROOT, "docs", dir);
+}
+
+function walkMarkdown(dir) {
+ if (!fs.existsSync(dir)) return [];
+ const out = [];
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) out.push(...walkMarkdown(full));
+ else if (entry.isFile() && entry.name.endsWith(".md")) out.push(full);
+ }
+ return out;
+}
+
+const violations = [];
+const files = [...new Set(readConfiguredGlobs().flatMap((g) => walkMarkdown(globToDir(g))))];
+
+for (const file of files) {
+ const rel = path.relative(ROOT, file);
+ const text = fs.readFileSync(file, "utf-8");
+
+ if (!text.startsWith("---")) {
+ violations.push(`${rel}: no frontmatter block (fumadocs requires a \`title\`)`);
+ continue;
+ }
+ const end = text.indexOf("\n---", 3);
+ if (end === -1) {
+ violations.push(`${rel}: frontmatter block is never closed`);
+ continue;
+ }
+ const frontmatter = text.slice(3, end);
+ const title = frontmatter.match(/^\s*title\s*:\s*(.+)$/m);
+ if (!title) {
+ violations.push(`${rel}: frontmatter has no \`title\``);
+ } else if (title[1].trim().replace(/^["']|["']$/g, "") === "") {
+ violations.push(`${rel}: \`title\` is empty`);
+ }
+}
+
+if (violations.length > 0) {
+ console.error(
+ `[docs-frontmatter] FAIL — ${violations.length} doc(s) would break the Next build:`
+ );
+ for (const v of violations) console.error(` - ${v}`);
+ console.error(
+ "\nEvery Markdown file matched by source.config.ts is compiled by fumadocs-mdx and needs a\n" +
+ 'frontmatter block with a title, e.g.:\n\n---\ntitle: "Removed Providers"\nversion: 3.8.51\nlastUpdated: 2026-09-03\n---\n'
+ );
+ process.exit(1);
+}
+
+console.log(
+ `[docs-frontmatter] OK — ${files.length} compiled doc(s) carry a valid frontmatter title.`
+);
diff --git a/scripts/check/check-known-symbols.ts b/scripts/check/check-known-symbols.ts
index e655c169bb..769afa5493 100644
--- a/scripts/check/check-known-symbols.ts
+++ b/scripts/check/check-known-symbols.ts
@@ -211,6 +211,8 @@ export const KNOWN_TRANSLATOR_PAIRS: readonly string[] = [
"antigravity:openai",
"claude:gemini",
"claude:openai",
+ // Naver CLOVA Studio Chat Completions v3 (native envelope, model in URL path).
+ "clova:openai",
"cursor:openai",
"gemini:claude",
"gemini:openai",
@@ -218,6 +220,7 @@ export const KNOWN_TRANSLATOR_PAIRS: readonly string[] = [
"openai-responses:openai",
"openai:antigravity",
"openai:claude",
+ "openai:clova",
"openai:cursor",
"openai:gemini",
"openai:kiro",
diff --git a/scripts/check/check-openapi-security-tiers.mjs b/scripts/check/check-openapi-security-tiers.mjs
index 812e4a0c1b..89d17c0482 100644
--- a/scripts/check/check-openapi-security-tiers.mjs
+++ b/scripts/check/check-openapi-security-tiers.mjs
@@ -1,9 +1,23 @@
#!/usr/bin/env node
/**
* Cross-references openapi.yaml x-loopback-only / x-always-protected annotations
- * against the compile-time constants in src/server/authz/routeGuard.ts.
+ * against the compile-time route-classification constants in
+ * src/server/authz/routeGuard.ts.
*
- * Fails if any YAML annotation disagrees with the routeGuard.ts constants.
+ * routeGuard classifies a loopback-only route through TWO mechanisms, and this
+ * checker must honor BOTH or it reports false positives (regression #12335):
+ *
+ * 1. LOCAL_ONLY_API_PREFIXES — flat string prefixes. One entry
+ * (VNC_ROUTE_PREFIX) is an imported const rather than a string literal, so
+ * it is resolved from its source module.
+ * 2. LOCAL_ONLY_API_PATTERNS — RegExp entries for spawn-capable routes whose
+ * dynamic path parameter sits BEFORE the gated segment (e.g.
+ * /api/providers/{id}/login), which a flat prefix cannot target without
+ * over-broadening the whole /api/providers/ subtree.
+ *
+ * A route is "covered" iff it matches a resolved prefix OR a pattern — exactly
+ * the `isLocalOnlyPath()` runtime contract. Fails if any YAML annotation
+ * disagrees with the routeGuard.ts constants.
*/
import fs from "node:fs";
@@ -13,34 +27,116 @@ import * as yaml from "js-yaml";
const ROOT = process.cwd();
const OPENAPI_PATH = path.join(ROOT, "docs", "openapi.yaml");
const ROUTE_GUARD_PATH = path.join(ROOT, "src", "server", "authz", "routeGuard.ts");
+const guardSrc = fs.readFileSync(ROUTE_GUARD_PATH, "utf-8");
-function parseStringArray(match) {
- if (!match) return [];
- // Strip line comments before splitting — array entries in routeGuard.ts often
- // carry inline `// T-XX:` annotations that would otherwise pollute the parsed tokens.
- return match[1]
- .replace(/\/\/[^\n]*/g, "")
- .split(",")
- .map((s) => s.trim().replace(/^["']|["']$/g, ""))
- .filter(Boolean);
+// Capture an exported array's body up to its closing `\n];`. Unlike a `[^\]]+`
+// capture, this is immune to `]` characters inside comments or regex character
+// classes (e.g. `[^/]`) — the exact footgun documented at routeGuard.ts's
+// /api/oauth/cursor/auto-import entry, and the reason regex patterns could not
+// be parsed at all before.
+function extractArrayBody(name) {
+ const m = guardSrc.match(
+ new RegExp(`export const ${name}\\b[\\s\\S]*?=\\s*\\[([\\s\\S]*?)\\n\\];`)
+ );
+ return m ? m[1] : null;
}
-const guardSrc = fs.readFileSync(ROUTE_GUARD_PATH, "utf-8");
-const LOCAL_ONLY_PREFIXES = parseStringArray(
- guardSrc.match(/export const LOCAL_ONLY_API_PREFIXES.*?=\s*\[([^\]]+)\]/s)
-);
-const ALWAYS_PROTECTED_PATHS = parseStringArray(
- guardSrc.match(/export const ALWAYS_PROTECTED_API_PATHS.*?=\s*\[([^\]]+)\]/s)
-);
+const stripLineComments = (s) => s.replace(/\/\/[^\n]*/g, "");
-if (LOCAL_ONLY_PREFIXES.length === 0 || ALWAYS_PROTECTED_PATHS.length === 0) {
- console.error("[openapi-security-tiers] FAIL — could not parse routeGuard.ts constants");
+function resolveModule(spec) {
+ let base;
+ if (spec.startsWith("@/")) base = path.join(ROOT, "src", spec.slice(2));
+ else if (spec.startsWith(".")) base = path.resolve(path.dirname(ROUTE_GUARD_PATH), spec);
+ else throw new Error(`openapi-security-tiers: unsupported import specifier '${spec}'`);
+ for (const cand of [base, `${base}.ts`, `${base}.mts`, path.join(base, "index.ts")]) {
+ if (fs.existsSync(cand) && fs.statSync(cand).isFile()) return cand;
+ }
+ throw new Error(`openapi-security-tiers: cannot resolve module '${spec}' (from ${base})`);
+}
+
+// Resolve a bare identifier used inside a prefix array (e.g. VNC_ROUTE_PREFIX)
+// to its string-literal value by following its import in routeGuard.ts.
+function resolveIdentifier(ident) {
+ const imp = guardSrc.match(
+ new RegExp(`import\\s*(?:type\\s*)?\\{[^}]*\\b${ident}\\b[^}]*\\}\\s*from\\s*["']([^"']+)["']`)
+ );
+ if (!imp)
+ throw new Error(
+ `openapi-security-tiers: '${ident}' used in a prefix array has no import in routeGuard.ts`
+ );
+ const modSrc = fs.readFileSync(resolveModule(imp[1]), "utf-8");
+ const lit = modSrc.match(new RegExp(`export const ${ident}\\s*=\\s*["']([^"']+)["']`));
+ if (!lit)
+ throw new Error(`openapi-security-tiers: cannot resolve '${ident}' to a string literal`);
+ return lit[1];
+}
+
+// String prefixes: quoted entries pass through; bare identifiers are resolved.
+function parsePrefixes(name) {
+ const body = extractArrayBody(name);
+ if (body == null)
+ throw new Error(`openapi-security-tiers: could not locate ${name} in routeGuard.ts`);
+ return stripLineComments(body)
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((tok) => {
+ const unquoted = tok.replace(/^["']|["']$/g, "");
+ return unquoted !== tok ? unquoted : resolveIdentifier(tok);
+ });
+}
+
+// RegExp patterns: one `/.../ ` literal per line.
+function parsePatterns(name) {
+ const body = extractArrayBody(name);
+ if (body == null)
+ throw new Error(`openapi-security-tiers: could not locate ${name} in routeGuard.ts`);
+ const out = [];
+ for (const raw of body.split("\n")) {
+ const t = raw
+ .replace(/\/\/.*$/, "")
+ .trim()
+ .replace(/,\s*$/, "")
+ .trim();
+ if (t.length > 2 && t.startsWith("/") && t.endsWith("/")) out.push(new RegExp(t.slice(1, -1)));
+ }
+ return out;
+}
+
+const LOCAL_ONLY_PREFIXES = parsePrefixes("LOCAL_ONLY_API_PREFIXES");
+const LOCAL_ONLY_PATTERNS = parsePatterns("LOCAL_ONLY_API_PATTERNS");
+const ALWAYS_PROTECTED_PATHS = parsePrefixes("ALWAYS_PROTECTED_API_PATHS");
+
+if (
+ LOCAL_ONLY_PREFIXES.length === 0 ||
+ LOCAL_ONLY_PATTERNS.length === 0 ||
+ ALWAYS_PROTECTED_PATHS.length === 0
+) {
+ console.error(
+ `[openapi-security-tiers] FAIL — could not parse routeGuard.ts constants ` +
+ `(prefixes=${LOCAL_ONLY_PREFIXES.length}, patterns=${LOCAL_ONLY_PATTERNS.length}, ` +
+ `alwaysProtected=${ALWAYS_PROTECTED_PATHS.length})`
+ );
process.exit(1);
}
+// OpenAPI template params ({id}, {sessionId}, …) → a concrete single non-slash
+// segment, so pattern regexes written against resolved paths (`[^/]+`) match.
+const concretize = (p) => p.replace(/\{[^}]+\}/g, "x");
+
+const matchesPrefix = (concrete) =>
+ LOCAL_ONLY_PREFIXES.some((prefix) => {
+ const norm = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
+ return concrete === norm || concrete.startsWith(`${norm}/`);
+ });
+
+function coveredByLocalOnly(pathStr) {
+ const concrete = concretize(pathStr);
+ return matchesPrefix(concrete) || LOCAL_ONLY_PATTERNS.some((re) => re.test(concrete));
+}
+
const raw = yaml.load(fs.readFileSync(OPENAPI_PATH, "utf-8"));
const paths = raw.paths || {};
-
const errors = [];
for (const [pathStr, methods] of Object.entries(paths)) {
@@ -48,17 +144,11 @@ for (const [pathStr, methods] of Object.entries(paths)) {
for (const [method, spec] of Object.entries(methods)) {
if (!["get", "post", "put", "patch", "delete"].includes(method) || !spec) continue;
- if (spec["x-loopback-only"] === true) {
- const matchesPrefix = LOCAL_ONLY_PREFIXES.some((prefix) => {
- const norm = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
- return pathStr === norm || pathStr.startsWith(norm + "/");
- });
- if (!matchesPrefix) {
- errors.push(
- `${method.toUpperCase()} ${pathStr}: has x-loopback-only but is NOT covered by ` +
- `LOCAL_ONLY_API_PREFIXES [${LOCAL_ONLY_PREFIXES.join(", ")}]`
- );
- }
+ if (spec["x-loopback-only"] === true && !coveredByLocalOnly(pathStr)) {
+ errors.push(
+ `${method.toUpperCase()} ${pathStr}: has x-loopback-only but is NOT covered by ` +
+ `LOCAL_ONLY_API_PREFIXES or LOCAL_ONLY_API_PATTERNS`
+ );
}
if (spec["x-always-protected"] === true) {
@@ -75,23 +165,13 @@ for (const [pathStr, methods] of Object.entries(paths)) {
}
}
-// Reverse pass: every YAML path that falls under a LOCAL_ONLY prefix should
-// carry `x-loopback-only: true` on every method, otherwise external API
-// consumers have no signal that the route is loopback-restricted. Closes the
-// "new spawn-capable route added without annotation" regression class.
-//
-// Currently reported as warnings (non-fatal) because the v3.8.4 release ships
-// with a known annotation gap on /api/services/* and /api/cli-tools/runtime/*
-// that will be patched in a follow-up doc-only PR. Promote to errors once the
-// backlog is cleared.
+// Reverse pass (non-fatal): every YAML path that falls under a LOCAL_ONLY prefix
+// should carry `x-loopback-only`. Pattern-only routes are intentionally excluded
+// — they are not "under" a broad prefix. Known annotation gaps stay warnings.
const reverseWarnings = [];
for (const [pathStr, methods] of Object.entries(paths)) {
if (!methods || typeof methods !== "object") continue;
- const fallsUnderLocalOnly = LOCAL_ONLY_PREFIXES.some((prefix) => {
- const norm = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
- return pathStr === norm || pathStr.startsWith(norm + "/");
- });
- if (!fallsUnderLocalOnly) continue;
+ if (!matchesPrefix(concretize(pathStr))) continue;
for (const [method, spec] of Object.entries(methods)) {
if (!["get", "post", "put", "patch", "delete"].includes(method) || !spec) continue;
if (spec["x-loopback-only"] !== true) {
@@ -105,7 +185,7 @@ for (const [pathStr, methods] of Object.entries(paths)) {
if (reverseWarnings.length > 0) {
console.warn(
- `[openapi-security-tiers] WARN — ${reverseWarnings.length} LOCAL_ONLY paths missing x-loopback-only annotation (non-fatal, follow-up doc PR):`
+ `[openapi-security-tiers] WARN — ${reverseWarnings.length} LOCAL_ONLY paths missing x-loopback-only annotation (non-fatal):`
);
reverseWarnings.forEach((w) => console.warn(` - ${w}`));
}
diff --git a/scripts/check/check-public-creds.mjs b/scripts/check/check-public-creds.mjs
index 7e065705d0..62549001bb 100644
--- a/scripts/check/check-public-creds.mjs
+++ b/scripts/check/check-public-creds.mjs
@@ -90,14 +90,18 @@ const ENV_KEY_RE = /(clientId|clientSecret|apiKey)Env\s*:/;
// The MiniMax family was extracted from services/usage.ts into services/usage/minimax.ts
// (god-file decomposition), so the FP moved with the getMiniMaxUsage signature.
//
-// open-sse/executors/zcodeProtocol.ts L302: `clientId: \`omniroute-${process.pid}\``
+// open-sse/executors/zcodeProtocol.ts L313: `clientId: \`omniroute-${process.pid}\``
// is the per-process identifier in the local ZCode app-server handshake. It is
// generated from the process PID, is not an upstream OAuth/client credential, and
// must remain visible in the wire contract. Frozen by file:line:value key.
+// NOTE: the key includes the LINE, so any edit that shifts this statement breaks
+// the gate twice over — a stale-entry error plus a "new violation" for the same
+// literal. That is what happened here (L302 -> L313). Re-point the line; do not
+// remove the entry.
export const KNOWN_LITERAL_CREDS = new Set([
"open-sse/services/usage/minimax.ts:213:minimax", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (getMiniMaxUsage signature)
"open-sse/services/usage/minimax.ts:213:minimax-cn", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (getMiniMaxUsage signature)
- "open-sse/executors/zcodeProtocol.ts:302:omniroute-${process.pid}", // local per-process ZCode handshake ID, not an upstream credential
+ "open-sse/executors/zcodeProtocol.ts:313:omniroute-${process.pid}", // local per-process ZCode handshake ID, not an upstream credential
]);
/**
diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx
index 8f67b95018..1a666b3634 100644
--- a/src/app/(dashboard)/dashboard/combos/page.tsx
+++ b/src/app/(dashboard)/dashboard/combos/page.tsx
@@ -761,15 +761,21 @@ function CombosPageContent() {
const [proxyConfig, setProxyConfig] = useState(null);
const { comboProxyAssignedIds, fetchComboProxyAssignments } = useComboProxyAssignments();
const [providerNodes, setProviderNodes] = useState([]);
- const [showUsageGuide, setShowUsageGuide] = useState(() => {
- // Lazy initializer instead of a mount effect (react-hooks/set-state-in-effect).
+ // SSR has no localStorage, so a lazy initializer reading it here returns a
+ // different value server-side (always "not dismissed") than the client's
+ // real stored value -- exactly the kind of source React's hydration
+ // mismatch check is built to catch, and in dev mode a mismatch forces a
+ // full client-only re-render of this tree, discarding whatever the fetch
+ // effects below had already populated. Start with the SSR-safe default on
+ // both passes and correct it client-only, after hydration, in an effect.
+ const [showUsageGuide, setShowUsageGuide] = useState(true);
+ useEffect(() => {
try {
- return globalThis.localStorage?.getItem(COMBO_USAGE_GUIDE_STORAGE_KEY) !== "1";
+ setShowUsageGuide(globalThis.localStorage?.getItem(COMBO_USAGE_GUIDE_STORAGE_KEY) !== "1");
} catch {
// Ignore storage access errors (privacy mode / restricted environments)
- return true;
}
- });
+ }, []);
const [recentlyCreatedCombo, setRecentlyCreatedCombo] = useState("");
const [creatingKimiPreset, setCreatingKimiPreset] = useState(false);
const [comboDragIndex, setComboDragIndex] = useState(null);
diff --git a/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx b/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx
index 4d7b506171..31bf43fff6 100644
--- a/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx
+++ b/src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx
@@ -3,12 +3,13 @@
// CompressionPanel — the single-source engine-grid UI for compression.
//
// Renders the master on/off switch, one row per catalog engine (on/off + level +
-// link to its detail page), the cavemanOutput intensity row, the mcpAccessibility
-// toggle (its own endpoint / separate store), a read-only derived-pipeline preview,
-// and the general settings (auto-trigger tokens + preserve-system-prompt).
+// link to its detail page), the adaptive context-budget dial, the cavemanOutput
+// intensity row, the mcpAccessibility toggle (its own endpoint / separate store),
+// a derived-pipeline preview, and the general settings (auto-trigger tokens +
+// preserve-system-prompt).
//
import Link from "next/link";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { useTranslations, useLocale } from "next-intl";
// Import Card/Toggle from their direct module paths rather than the @/shared/components
// barrel: the barrel transitively pulls a heavy/Node-only module that hangs the
@@ -60,12 +61,20 @@ interface CompressionConfig {
// Best-effort pre-warm of the SLM model on enable / cold restart. Default false.
ultraSlmPrewarm?: boolean;
// Phase 4 (C): adaptive context-budget. Absent / mode:"off" = legacy auto-trigger.
- // The panel currently surfaces the computed target read-only; mode/policy editors are a
- // follow-up (the load/save path does not yet populate this field).
contextBudget?: ContextBudgetConfig;
liveZone?: { enabled: boolean };
}
+const CONTEXT_BUDGET_MODES = new Set([
+ "off",
+ "floor",
+ "replace-autotrigger",
+]);
+const CONTEXT_BUDGET_POLICIES = new Set([
+ "reserve-output",
+ "percentage",
+ "absolute",
+]);
const CAVEMAN_OUTPUT_LEVELS: CavemanIntensity[] = ["lite", "full", "ultra"];
const DEFAULT_CONFIG: CompressionConfig = {
@@ -78,6 +87,7 @@ const DEFAULT_CONFIG: CompressionConfig = {
outputStyles: [],
ultraEngine: "heuristic",
ultraSlmPrewarm: false,
+ contextBudget: { ...DEFAULT_CONTEXT_BUDGET },
liveZone: { enabled: false },
};
@@ -120,22 +130,73 @@ function LiveZoneToggle({
);
}
-function AdaptiveTargetPreview({ contextBudget }: { contextBudget?: ContextBudgetConfig }) {
+function AdaptiveContextBudgetDial({
+ contextBudget,
+ saving,
+ onChange,
+}: {
+ contextBudget: ContextBudgetConfig;
+ saving: boolean;
+ onChange: (patch: Partial) => void;
+}) {
const t = useTranslations("settings");
- const target = getAdaptiveTargetSummary(contextBudget ?? DEFAULT_CONTEXT_BUDGET, 200000);
+ // Representative window for the preview label (D-C1). Not the live model limit —
+ // the panel has no selected-model context here; 200k is Claude-class default.
+ const target = getAdaptiveTargetSummary(contextBudget, 200000);
return (
-
- {target.enabled
- ? t("compressionAdaptiveTarget", {
- mode: target.mode,
- policy: target.policy,
- target: target.target,
- contextLimit: target.contextLimit,
- })
- : t("compressionAdaptiveOff")}
+
+
+ {t("compressionAdaptiveMode")}
+ {
+ const mode = e.target.value;
+ if (CONTEXT_BUDGET_MODES.has(mode as ContextBudgetConfig["mode"])) {
+ onChange({ mode: mode as ContextBudgetConfig["mode"] });
+ }
+ }}
+ disabled={saving}
+ className="w-44 rounded border border-border bg-surface px-2 py-1 text-sm text-text-main"
+ >
+ {t("compressionAdaptiveModeOff")}
+ {t("compressionAdaptiveModeFloor")}
+ {t("compressionAdaptiveModeReplace")}
+
+
+ {(contextBudget.mode ?? "off") !== "off" && (
+
+
+ {t("compressionAdaptivePolicy")}
+
+ {
+ const policy = e.target.value;
+ if (CONTEXT_BUDGET_POLICIES.has(policy as ContextBudgetConfig["policy"])) {
+ onChange({ policy: policy as ContextBudgetConfig["policy"] });
+ }
+ }}
+ disabled={saving}
+ className="w-44 rounded border border-border bg-surface px-2 py-1 text-sm text-text-main"
+ >
+ {t("compressionAdaptivePolicyReserve")}
+ {t("compressionAdaptivePolicyPercentage")}
+ {t("compressionAdaptivePolicyAbsolute")}
+
+
+ )}
+
+ {target.enabled
+ ? t("compressionAdaptiveTarget", {
+ mode: target.mode,
+ policy: target.policy,
+ target: target.target,
+ contextLimit: target.contextLimit,
+ })
+ : t("compressionAdaptiveOff")}
+
);
}
@@ -153,19 +214,29 @@ export default function CompressionPanel() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<"" | "saved" | "error">("");
+ const configRef = useRef(config);
+ useEffect(() => {
+ configRef.current = config;
+ }, [config]);
+ const saveGenRef = useRef(0);
+ const lastConfirmedRef = useRef(config);
+ const lastAckedGenRef = useRef(0);
useEffect(() => {
fetch("/api/settings/compression")
.then((r) => (r.ok ? r.json() : null))
.then((data: Partial
| null) => {
if (data) {
- setConfig({
+ const hydrated: CompressionConfig = {
...DEFAULT_CONFIG,
...data,
engines: normalizeEngines(data.engines),
cavemanOutputMode: data.cavemanOutputMode ?? DEFAULT_CONFIG.cavemanOutputMode,
outputStyles: data.outputStyles ?? DEFAULT_CONFIG.outputStyles,
- });
+ contextBudget: { ...DEFAULT_CONTEXT_BUDGET, ...(data.contextBudget ?? {}) },
+ };
+ lastConfirmedRef.current = hydrated;
+ setConfig(hydrated);
}
})
.catch(() => {})
@@ -181,8 +252,24 @@ export default function CompressionPanel() {
// Persist a merge-patch. The DB persists `engines` as one whole row, so callers that
// touch an engine pass the full engines map to avoid dropping the other engines.
+ // Generation + configRef: a later in-flight save must not let an older failure
+ // roll back a newer optimistic (or already-acked) state.
const save = async (updates: Partial) => {
- const next = { ...config, ...updates };
+ const gen = ++saveGenRef.current;
+ const previous = configRef.current;
+ const next: CompressionConfig = {
+ ...previous,
+ ...updates,
+ ...(updates.contextBudget
+ ? {
+ contextBudget: {
+ ...(previous.contextBudget ?? DEFAULT_CONTEXT_BUDGET),
+ ...updates.contextBudget,
+ },
+ }
+ : {}),
+ };
+ configRef.current = next;
setConfig(next);
setSaving(true);
setStatus("");
@@ -192,16 +279,34 @@ export default function CompressionPanel() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
- if (res.ok) {
- setStatus("saved");
- setTimeout(() => setStatus(""), 2000);
- } else {
- setStatus("error");
+ // Acked server state is recorded even when this gen is stale, so a
+ // later failure rolls back to the newest acked PUT, not the GET.
+ // lastAckedGenRef stops an older ack from overwriting a newer one.
+ if (res.ok && gen >= lastAckedGenRef.current) {
+ lastConfirmedRef.current = next;
+ lastAckedGenRef.current = gen;
+ }
+ if (gen === saveGenRef.current) {
+ if (res.ok) {
+ setStatus("saved");
+ const savedGen = gen;
+ setTimeout(() => {
+ if (savedGen === saveGenRef.current) setStatus("");
+ }, 2000);
+ } else {
+ configRef.current = lastConfirmedRef.current;
+ setConfig(lastConfirmedRef.current);
+ setStatus("error");
+ }
}
} catch {
- setStatus("error");
+ if (gen === saveGenRef.current) {
+ configRef.current = lastConfirmedRef.current;
+ setConfig(lastConfirmedRef.current);
+ setStatus("error");
+ }
} finally {
- setSaving(false);
+ if (gen === saveGenRef.current) setSaving(false);
}
};
@@ -326,8 +431,15 @@ export default function CompressionPanel() {
{derivedText}
- {/* Adaptive context-budget — read-only computed target (Phase 4C, D-C1 transparency) */}
-
+ {/* Adaptive context-budget dial — mode/policy persist via PUT contextBudget */}
+ {
+ const current = configRef.current.contextBudget ?? DEFAULT_CONTEXT_BUDGET;
+ save({ contextBudget: { ...current, ...patch } });
+ }}
+ />
{/* Engine grid */}
diff --git a/src/app/(dashboard)/dashboard/conversations/page.tsx b/src/app/(dashboard)/dashboard/conversations/page.tsx
index f9fb3b1848..14b8bdf4d7 100644
--- a/src/app/(dashboard)/dashboard/conversations/page.tsx
+++ b/src/app/(dashboard)/dashboard/conversations/page.tsx
@@ -26,6 +26,11 @@ interface ConversationRow {
// streaming (call_logs only gets its row on completion). Used to poll
// /api/logs/[id] for this conversation's live partial assistant text.
activeCallLogId: string | null;
+ // Whether the latest turn actually used previous_response_id and it
+ // resolved server-side — distinct from this row existing at all, which
+ // only means the client-side content-hash tracker saw >= 2 turns
+ // regardless of transport (see isGenuineContinuationTurn).
+ isGenuineContinuation: boolean;
}
// Same spinner used for an in-flight request on /dashboard/logs
@@ -101,6 +106,23 @@ function StatusBadge({ status }: { status: number | null }) {
);
}
+// Distinguishes a conversation whose latest turn actually used
+// previous_response_id (server-verified — see isGenuineContinuationTurn)
+// from one the content-hash tracker merely counts as multi-turn while still
+// resending full history each request.
+function ContinuationBadge({ isGenuine }: { isGenuine: boolean }) {
+ if (!isGenuine) return null;
+ return (
+
+ bolt
+ continuation
+
+ );
+}
+
/**
* Builds the exact NormalizedBlock (src/mitm/inspector/types.ts) the
* request-detail panel already builds from buildRequestTurns/
@@ -268,13 +290,15 @@ function ConversationsPageContent() {
// itself in the poll effect's dependency array (which would tear down and
// restart the interval on every single appended turn).
const newestSeqRef = useRef
(null);
+ // Tracks the PREVIOUS render's activeCallLogId truthiness, so the
+ // reply-just-finished effect below can detect the true->false transition
+ // specifically (not "is currently falsy", which would also fire on mount
+ // / switching conversations).
+ const wasReplyActiveRef = useRef(false);
- // Extracted so openConversation can force an immediate refresh instead of
- // waiting for the next scheduled tick — see its call site for why: a
- // conversation opened right after a new reply starts streaming otherwise
- // shows no live text until this poll's own interval happens to land,
- // because activeCallLogId only updates via the resync effect below, which
- // depends on this list actually having been refetched.
+ // The background list poll below only runs this while no conversation
+ // modal is open — see loadActiveConversationSummary and the poll effect
+ // for the lighter single-row path used while one is open.
const loadConversations = useCallback(() => {
if (document.visibilityState !== "visible") return;
return fetch("/api/conversations?limit=100", { cache: "no-store" })
@@ -290,22 +314,51 @@ function ConversationsPageContent() {
});
}, []);
+ // While the modal is open, only the one open conversation's summary needs
+ // to stay live (see the resync effect below) — refetching and
+ // re-annotating the whole up-to-100-row list every poll tick just to pluck
+ // that one row back out is pure waste, and at a 1s poll interval it's
+ // waste on every tick. Patches the row in place so the existing resync
+ // effect (keyed on `conversations`) picks it up unchanged.
+ const loadActiveConversationSummary = useCallback((id: string) => {
+ if (document.visibilityState !== "visible") return;
+ return fetch(`/api/conversations/${id}`, { cache: "no-store" })
+ .then((res) => (res.ok ? res.json() : null))
+ .then((data) => {
+ const fresh = data?.conversation;
+ if (!fresh) return;
+ setConversations((prev) => {
+ const idx = prev.findIndex((c) => c.id === fresh.id);
+ if (idx === -1) return prev;
+ const next = prev.slice();
+ next[idx] = fresh;
+ return next;
+ });
+ })
+ .catch(() => {});
+ }, []);
+
useEffect(() => {
- loadConversations();
- const interval = setInterval(loadConversations, pollSeconds * 1000);
+ const poll = () =>
+ activeConversationId
+ ? loadActiveConversationSummary(activeConversationId)
+ : loadConversations();
+ poll();
+ const interval = setInterval(poll, pollSeconds * 1000);
return () => {
clearInterval(interval);
};
- }, [pollSeconds, loadConversations]);
+ }, [pollSeconds, loadConversations, loadActiveConversationSummary, activeConversationId]);
// activeConversation is a snapshot taken once at openConversation() time —
// it's never touched again while the modal stays open (the turns-poll
// effect below only appends conversationNodes). Without this, "Goto latest
// request" and any other displayed summary field (lastModel/lastStatus/
// turnCount) go stale the moment a new request lands in this conversation
- // while you're still reading it, even though the list poll above (which
- // runs regardless of whether the modal is open) already has the fresh
- // row. Re-sync from it whenever the list refreshes.
+ // while you're still reading it. Re-synced from `conversations` whenever
+ // that refreshes — the effect above keeps it fresh whether the modal is
+ // closed (full list poll) or open (single-conversation poll patches this
+ // same row in place).
useEffect(() => {
if (!activeConversationId) return;
const fresh = conversations.find((c) => c.id === activeConversationId);
@@ -461,13 +514,13 @@ function ConversationsPageContent() {
// ignore navigation errors
}
// `row` is a snapshot from whenever the list last polled — if a reply
- // started streaming after that tick, row.activeCallLogId is still
- // null and the live-text poll effect never starts until the next
- // scheduled list refresh happens to land (the exact "opened it and
- // saw nothing, closed and reopened and saw it live" report). Force
- // one now so activeConversation resyncs with the current isActive/
- // activeCallLogId immediately instead of waiting on pollSeconds.
- loadConversations();
+ // started streaming after that tick, row.activeCallLogId is still null
+ // and the live-text poll effect never starts until a fresh summary
+ // lands (the exact "opened it and saw nothing, closed and reopened and
+ // saw it live" report). setActiveConversation above already changes
+ // activeConversationId, which is a dependency of the poll effect below
+ // — it tears down and re-fires immediately on that change, forcing the
+ // single-row resync here for free without a second explicit call.
fetchConversationPage(row.id, `limit=${CONVERSATION_PAGE_SIZE}`)
.then((page) => {
setConversationNodes(page?.nodes ?? []);
@@ -480,7 +533,7 @@ function ConversationsPageContent() {
scrollToBottom();
});
},
- [router, fetchConversationPage, scrollToBottom, loadConversations]
+ [router, fetchConversationPage, scrollToBottom]
);
const closeConversation = useCallback(() => {
@@ -583,6 +636,35 @@ function ConversationsPageContent() {
return () => clearInterval(interval);
}, [activeConversationId, pollSeconds, fetchConversationPage]);
+ // Live incident (2026-09-02): resolveConversationId reassigns a node's
+ // last_correlation_id to the CURRENT request at request-START (before its
+ // reply streams), but that request's call-log artifact -- what
+ // resolveTurnDisplayContent needs to show real text -- is only written at
+ // completion. A node touched by a still-in-flight request therefore
+ // legitimately resolves empty if fetched during that window; the afterSeq
+ // poll above only ever APPENDS strictly newer nodes, so one already
+ // rendered empty stays empty in local state forever, even once its
+ // artifact exists moments later -- the exact "empty until you close and
+ // reopen the conversation" symptom. Once a reply that was streaming
+ // finishes (activeCallLogId's true -> false transition -- see the
+ // wasReplyActiveRef doc comment), re-fetch the recent page and merge it in
+ // by id (never drop older "Load more" history) so any node that resolved
+ // empty during the race gets its real content without a manual reopen.
+ useEffect(() => {
+ const wasActive = wasReplyActiveRef.current;
+ wasReplyActiveRef.current = Boolean(activeCallLogId);
+ if (!wasActive || activeCallLogId || !activeConversationId) return;
+
+ fetchConversationPage(activeConversationId, `limit=${CONVERSATION_PAGE_SIZE}`).then((page) => {
+ if (!page || page.nodes.length === 0) return;
+ setConversationNodes((prev) => {
+ const byId = new Map(prev.map((n) => [n.id, n] as const));
+ for (const n of page.nodes) byId.set(n.id, n);
+ return [...byId.values()].sort((a, b) => a.seq - b.seq);
+ });
+ });
+ }, [activeCallLogId, activeConversationId, fetchConversationPage]);
+
// Live preview of the CURRENTLY streaming reply, if any: conversation_turn_nodes
// only gains a node for an assistant turn once the client resends it as
// history on its NEXT request (resolveConversationId reads only the request
@@ -655,6 +737,7 @@ function ConversationsPageContent() {
lastStatus: null,
isActive: false,
activeCallLogId: null,
+ isGenuineContinuation: false,
}
);
}, [initialConversationParam, loading, conversations, openConversation]);
@@ -759,6 +842,7 @@ function ConversationsPageContent() {
>
{row.id.slice(0, 16)}…
+
{row.turnCount} turns
@@ -785,6 +869,7 @@ function ConversationsPageContent() {
Conversation
Turns
+ Continuation
Last Model
Provider
Status
@@ -816,6 +901,9 @@ function ConversationsPageContent() {
{row.turnCount}
+
+
+
{row.lastModel ?? "—"}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx
index 026aa324fa..dc1966677f 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx
@@ -15,6 +15,8 @@ interface EditCompatibleNodeModalNode {
chatPath?: string;
modelsPath?: string;
iconUrl?: string;
+ dailyQuotaResetTimezone?: string | null;
+ dailyQuotaResetHour?: number | null;
providerSpecificData?: Record;
}
@@ -48,6 +50,8 @@ export default function EditCompatibleNodeModal({
consoleApiKey: "",
newApiUserId: "",
quotaPerUnit: "",
+ dailyQuotaResetTimezone: "",
+ dailyQuotaResetHour: "",
});
const [saving, setSaving] = useState(false);
const [checkKey, setCheckKey] = useState("");
@@ -98,6 +102,11 @@ export default function EditCompatibleNodeModal({
consoleApiKey: typeof psd.consoleApiKey === "string" ? psd.consoleApiKey : "",
newApiUserId: typeof psd.newApiUserId === "string" ? psd.newApiUserId : "",
quotaPerUnit: typeof psd.quotaPerUnit === "number" ? String(psd.quotaPerUnit) : "",
+ dailyQuotaResetTimezone: node.dailyQuotaResetTimezone || "",
+ dailyQuotaResetHour:
+ node.dailyQuotaResetHour === 0 || node.dailyQuotaResetHour
+ ? String(node.dailyQuotaResetHour)
+ : "",
});
setSaveError(null);
setIconUrlError(null);
@@ -141,6 +150,10 @@ export default function EditCompatibleNodeModal({
modelsPath: isCcCompatible ? "" : formData.modelsPath,
iconUrl: formData.iconUrl.trim(),
};
+ const tz = formData.dailyQuotaResetTimezone.trim();
+ payload.dailyQuotaResetTimezone = tz || null;
+ const hourRaw = formData.dailyQuotaResetHour.trim();
+ payload.dailyQuotaResetHour = hourRaw === "" ? null : Number(hourRaw);
if (!isAnthropic) {
payload.apiType = formData.apiType;
}
@@ -345,6 +358,22 @@ export default function EditCompatibleNodeModal({
hint={t("modelsPathHint")}
/>
)}
+