From b100325fe0e34ff2cbf20eb3e0b78929d25babbf Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 5 Apr 2026 02:54:44 -0300 Subject: [PATCH] =?UTF-8?q?chore(release):=20v3.5.2=20=E2=80=94=20Qoder=20?= =?UTF-8?q?DashScope=20Native=20Integration=20&=20Stability=20(#999)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(qoder): native cosy integration * feat(qoder): implement native COSY encryption algorithm and remove CLI child instances, plus workflow bumps * feat(resilience): context overflow fallback, OAuth token detection, empty content guard & context-optimized combo strategy - Add isContextOverflowError + isContextOverflow detectors (400 + token-limit signals) - Auto-fallback to next family model on context overflow in chatCore - Add isEmptyContentResponse to catch fake-success empty responses, trigger fallback + recursive retry - Add OAUTH_INVALID_TOKEN error type (T11) with isOAuthInvalidToken signal matching; warn instead of deactivating node - Add getModelContextLimit helper in modelsDevSync (reads limit_context from synced capabilities) - Upgrade getTokenLimit in contextManager to check models.dev DB before registry (fixes gemini-2.5-pro: 1000000→1048576) - Add findLargerContextModel in modelFamilyFallback for context-aware model selection - Add sortModelsByContextSize + context-optimized combo strategy in combo.ts - Update context-manager unit test for corrected gemini-2.5-pro limit Co-Authored-By: Claude Sonnet 4.6 * fix(review): address Gemini code review — tool_calls path, infinite recursion, dedup signals, findLargerContextModel - Fix isEmptyContentResponse: check message.tool_calls/delta.tool_calls instead of firstChoice.tool_calls (wrong OpenAI API path, caused tool-call responses to be falsely flagged as empty) - Fix empty content fallback: replace recursive handleChatCore call (infinite recursion risk + wrong model due to original body.model) with non-recursive pattern — call executeProviderRequest, parse fallback response body, reassign responseBody and fall through to existing processing - Fix context overflow: use findLargerContextModel over family candidates first, fall back to getNextFamilyFallback — ensures we pick a model with actually larger context window on overflow - Fix signal dedup: export CONTEXT_OVERFLOW_SIGNALS + CONTEXT_OVERFLOW_REGEX from errorClassifier.ts; import shared regex in modelFamilyFallback.ts, removing duplicate signal list and per-call RegExp construction Co-Authored-By: Claude Sonnet 4.6 * fix(UI): add context-optimized strategy to frontend schema and options * fix(sse): preserve Responses API events in stream translation When translating Claude-format responses (e.g. GLM) to Responses API format for Codex CLI, the sanitizer stripped {event, data} structured items to {"object":"chat.completion.chunk"}, losing all content and the critical response.completed event. Only run sanitizeStreamingChunk on OpenAI Chat Completions chunks, skipping items that have the Responses API {event, data} structure. * test(sse): add regression test for Claude→Responses stream sanitization Verifies that {event,data} structured items from the Responses API translator bypass sanitizeStreamingChunk when translating Claude-format providers (e.g. GLM) to Responses API format for Codex CLI. * fix(sse): strengthen Responses API event detection with response. prefix check Use explicit `response.` prefix check instead of generic `event && data` presence check, as recommended in PR review. * fix: pin Next.js to 16.0.10 to prevent Turbopack hashed module bug Remove ^ prefix from next and eslint-config-next to prevent automatic upgrades to 16.1.x+ which introduced content-based hashing for external module references in Turbopack. Also remove duplicate Material Symbols @import from globals.css (font already loaded via in layout.tsx). Fixes #509 * align cc-compatible cache handling with client passthrough * chore: integrate resilience and turbopack fixes (PRs #992, #990, #987) * chore(release): bump to v3.5.2 — changelog, docs, version sync * docs(i18n): sync documentation updates to 33 languages * fix(qoder): replace any with unknown to comply with strict any-budget --------- Co-authored-by: diegosouzapw Co-authored-by: oyi77 Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Chris Staley Co-authored-by: Ivan Co-authored-by: R.D. --- .agents/workflows/deploy-vps-akamai.md | 2 +- .agents/workflows/deploy-vps-both.md | 2 +- .agents/workflows/deploy-vps-local.md | 2 +- .agents/workflows/generate-release.md | 6 +- CHANGELOG.md | 16 + docs/i18n/ar/CHANGELOG.md | 19 + docs/i18n/bg/CHANGELOG.md | 19 + docs/i18n/cs/CHANGELOG.md | 19 + docs/i18n/da/CHANGELOG.md | 19 + docs/i18n/de/CHANGELOG.md | 19 + docs/i18n/es/CHANGELOG.md | 19 + docs/i18n/fi/CHANGELOG.md | 19 + docs/i18n/fr/CHANGELOG.md | 19 + docs/i18n/he/CHANGELOG.md | 19 + docs/i18n/hi/CHANGELOG.md | 19 + docs/i18n/hu/CHANGELOG.md | 19 + docs/i18n/id/CHANGELOG.md | 19 + docs/i18n/in/CHANGELOG.md | 19 + docs/i18n/it/CHANGELOG.md | 19 + docs/i18n/ja/CHANGELOG.md | 19 + docs/i18n/ko/CHANGELOG.md | 19 + docs/i18n/ms/CHANGELOG.md | 19 + docs/i18n/nl/CHANGELOG.md | 19 + docs/i18n/no/CHANGELOG.md | 19 + docs/i18n/phi/CHANGELOG.md | 19 + docs/i18n/pl/CHANGELOG.md | 19 + docs/i18n/pt-BR/CHANGELOG.md | 19 + docs/i18n/pt/CHANGELOG.md | 19 + docs/i18n/ro/CHANGELOG.md | 19 + docs/i18n/ru/CHANGELOG.md | 19 + docs/i18n/sk/CHANGELOG.md | 19 + docs/i18n/sv/CHANGELOG.md | 19 + docs/i18n/th/CHANGELOG.md | 19 + docs/i18n/tr/CHANGELOG.md | 19 + docs/i18n/uk-UA/CHANGELOG.md | 19 + docs/i18n/vi/CHANGELOG.md | 19 + docs/i18n/zh-CN/CHANGELOG.md | 19 + docs/openapi.yaml | 2 +- electron/package.json | 2 +- llm.txt | 4 +- open-sse/executors/qoder.ts | 359 +- open-sse/handlers/chatCore.ts | 141 +- open-sse/package.json | 2 +- open-sse/services/accountFallback.ts | 18 + open-sse/services/claudeCodeCompatible.ts | 159 +- open-sse/services/combo.ts | 22 + open-sse/services/contextManager.ts | 15 +- open-sse/services/errorClassifier.ts | 93 +- open-sse/services/modelFamilyFallback.ts | 41 + open-sse/services/qoderCli.ts | 208 +- open-sse/utils/cacheControlPolicy.ts | 7 - open-sse/utils/stream.ts | 6 +- package-lock.json | 268 +- package.json | 6 +- scripts/prepublish.mjs | 2 +- src/app/globals.css | 1 - src/lib/modelsDevSync.ts | 9 + src/shared/constants/routingStrategies.ts | 9 + src/shared/validation/schemas.ts | 1 + test-antigrav.mjs | 43 - test-executor.ts | 38 + test-mcp-bundle.js | 3923 ----------------- test-port.js | 28 - test_translator.mjs | 9 - .../cache-control-claude-providers.test.mjs | 4 +- tests/unit/cc-compatible-provider.test.mjs | 77 +- .../unit/claude-code-rendering-fixes.test.mjs | 66 + tests/unit/context-manager.test.mjs | 9 +- 68 files changed, 1384 insertions(+), 4824 deletions(-) delete mode 100644 test-antigrav.mjs create mode 100644 test-executor.ts delete mode 100644 test-mcp-bundle.js delete mode 100644 test-port.js delete mode 100644 test_translator.mjs diff --git a/.agents/workflows/deploy-vps-akamai.md b/.agents/workflows/deploy-vps-akamai.md index 846dbd0cf3..50f92cab5f 100644 --- a/.agents/workflows/deploy-vps-akamai.md +++ b/.agents/workflows/deploy-vps-akamai.md @@ -17,7 +17,7 @@ Deploy OmniRoute to the Akamai VPS using `npm pack + scp` + PM2. // turbo ```bash -cd /home/diegosouzapw/dev/proxys/9router && npm run build:cli && npm pack --ignore-scripts +cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts ``` ### 2. Copy to Akamai VPS and install diff --git a/.agents/workflows/deploy-vps-both.md b/.agents/workflows/deploy-vps-both.md index 0fc2a1fc2d..161d197bd4 100644 --- a/.agents/workflows/deploy-vps-both.md +++ b/.agents/workflows/deploy-vps-both.md @@ -22,7 +22,7 @@ Deploy OmniRoute to the production VPSs using `npm pack + scp` + PM2. // turbo ```bash -cd /home/diegosouzapw/dev/proxys/9router && npm run build:cli && npm pack --ignore-scripts +cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts ``` ### 2. Copy to both VPS and install diff --git a/.agents/workflows/deploy-vps-local.md b/.agents/workflows/deploy-vps-local.md index 90545aae7a..f348f63da5 100644 --- a/.agents/workflows/deploy-vps-local.md +++ b/.agents/workflows/deploy-vps-local.md @@ -17,7 +17,7 @@ Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2. // turbo ```bash -cd /home/diegosouzapw/dev/proxys/9router && npm run build:cli && npm pack --ignore-scripts +cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts ``` ### 2. Copy to Local VPS and install diff --git a/.agents/workflows/generate-release.md b/.agents/workflows/generate-release.md index 8eff07ea89..bbc5aca09b 100644 --- a/.agents/workflows/generate-release.md +++ b/.agents/workflows/generate-release.md @@ -238,15 +238,15 @@ gh workflow run docker-publish.yml --repo diegosouzapw/OmniRoute --ref v2.x.y ```bash # Build and pack locally -cd /home/diegosouzapw/dev/proxys/9router && npm run build:cli && npm pack --ignore-scripts +cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts # Deploy to LOCAL VPS (192.168.0.15) scp omniroute-*.tgz root@192.168.0.15:/tmp/ -ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && pm2 restart omniroute && pm2 save" +ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'" # Deploy to AKAMAI VPS (69.164.221.35) scp omniroute-*.tgz root@69.164.221.35:/tmp/ -ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && pm2 restart omniroute && pm2 save" +ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" # Verify both curl -s -o /dev/null -w "LOCAL: HTTP %{http_code}\n" http://192.168.0.15:20128/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 326aba3edc..032380be97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 26aea5c60c..707fafc00d 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index b3d05831dc..1627c5610f 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index 397d80e009..16b32f44d2 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 836a0355e0..454de29c0b 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index f5c2a41018..3ca7ace498 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index 5b69741ef7..cd53a8be90 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 08cdb1c1fc..ce2cdd451a 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index ae8d518a8d..a8ae0a85c0 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index 4ff7a3dc66..967510c9e2 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 21aae3b223..eee98f2856 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index 4d2ede05e3..d59abe697c 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index 566d09c98e..e5e448104b 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index b42200d604..3ffdaa81f1 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index 4ebd9101e7..df4265a435 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index 82a7cba246..d419278479 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index ce602b5f00..d3e554bd2a 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 8bc9df45c2..6e8c3204c9 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index c584f744f1..9cdf92d54c 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 4ecf3f2c84..253913c6f9 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index 8e61ed620b..ab4ac2bd28 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 97e73608a2..475d5e1329 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 7e832529d4..0935f08c8c 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index b45b5e4671..e41de8df14 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 4709ccd9c8..f5bdef2931 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index 6540f30814..6f5338d250 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index eecbabe5c5..c1d0e38e54 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index cf3c049ba5..9fdc082321 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index 01e75aaea5..4edef9a421 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index ab82a22edd..09ef3c02f0 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index a741a48a94..1c1ce154b6 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index ecce0c711e..036914ad92 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 18cf53b1a9..643cb2926d 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -4,14 +4,33 @@ --- + ## [Unreleased] --- +## [3.5.2] — 2026-04-05 + +### ✨ New Features + +- **Qoder API Native Integration:** Completely refactored the Qoder Executor to bypass the legacy COSY AES/RSA encryption algorithm, routing directly into the native DashScope OpenAi-compatible URL. Eliminates complex dependencies on Node `crypto` modules while improving stream fidelity. +- **Resilience Engine Overhaul:** Integrated context overflow graceful fallbacks, proactive OAuth token detection, and empty-content emission prevention (#990). +- **Context-Optimized Routing Strategy:** Added new intelligent routing capability to natively maximize context windows in automated combo deployments (#990). + +### 🐛 Bug Fixes + +- **Responses API Stream Corruption:** Fixed deep-cloning corruption where Anthropic/OpenAI translation boundaries stripped `response.` specific SSE prefixes from streaming boundaries (#992). +- **Claude Cache Passthrough Alignment:** Aligned CC-Compatible cache markers consistently with upstream Client Pass-Through mode preserving prompt caching. +- **Turbopack Memory Leak:** Pinned Next.js to strict `16.0.10` preventing memory leaks and build staleness from recent upstream Turbopack hashed module regressions (#987). + +--- + ## [3.5.1] — 2026-04-04 ### ✨ New Features +- **Models.dev Integration:** Integrated models.dev as the authoritative runtime source for model pricing, capabilities, and specifications, overriding hardcoded prices. Includes a settings UI to manage sync intervals, translation strings for all 30 languages, and robust test coverage. +- **Provider Native Capabilities:** Added support for declaring and checking native API features (e.g. `systemInstructions_supported`) preventing failures by sanitizing invalid roles. Currently configured for Gemini Base and Antigravity OAuth providers. - **API Provider Advanced Settings:** Added per-connection custom `User-Agent` overrides for API-key provider connections. The override is stored in `providerSpecificData.customUserAgent` and now applies to validation probes and upstream execution requests. ### 🐛 Bug Fixes diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0bd1da7dd5..175708b2e7 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.5.1 + version: 3.5.2 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/electron/package.json b/electron/package.json index c68817142a..283d97a02e 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.5.1", + "version": "3.5.2", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/llm.txt b/llm.txt index f93d30e37e..4ecfa2417b 100644 --- a/llm.txt +++ b/llm.txt @@ -8,7 +8,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo **Key value:** One endpoint (`http://localhost:20128/v1`), unlimited models, zero downtime, minimal cost. -**Current version:** 3.5.0 +**Current version:** 3.5.2 ## Tech Stack @@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo └── .env.example # Environment variable template ``` -## Key Features (v3.5.0) +## Key Features (v3.5.2) ### Core Proxy - **60+ AI providers** with automatic format translation diff --git a/open-sse/executors/qoder.ts b/open-sse/executors/qoder.ts index 44c2a59037..77139b821f 100644 --- a/open-sse/executors/qoder.ts +++ b/open-sse/executors/qoder.ts @@ -1,5 +1,3 @@ -import crypto from "crypto"; -import { spawn } from "child_process"; import { BaseExecutor, mergeUpstreamExtraHeaders, @@ -7,23 +5,17 @@ import { type ProviderCredentials, } from "./base.ts"; import { PROVIDERS } from "../config/constants.ts"; -import { - buildQoderChunk, - buildQoderCompletionPayload, - buildQoderPrompt, - createQoderErrorResponse, - extractTextFromQoderEnvelope, - getQoderCliCommand, - getQoderCliWorkspace, - mapQoderModelToLevel, - parseQoderCliFailure, - runQoderCliCommand, -} from "../services/qoderCli.ts"; -function getPat(credentials: ProviderCredentials): string { +function getAuthToken(credentials: ProviderCredentials): string { if (typeof credentials.apiKey === "string" && credentials.apiKey.trim()) { return credentials.apiKey.trim(); } + if (typeof credentials.accessToken === "string" && credentials.accessToken.trim()) { + return credentials.accessToken.trim(); + } + if (typeof credentials.refreshToken === "string" && credentials.refreshToken.trim()) { + return credentials.refreshToken.trim(); + } return ""; } @@ -32,257 +24,134 @@ export class QoderExecutor extends BaseExecutor { super("qoder", PROVIDERS.qoder); } - buildHeaders(_credentials: ProviderCredentials, stream = true): Record { - return { - "Content-Type": "application/json", - ...(stream ? { Accept: "text/event-stream" } : {}), - }; - } - async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }: ExecuteInput) { - const headers = this.buildHeaders(credentials, stream); - mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); + const token = getAuthToken(credentials); - const pat = getPat(credentials); - if (!pat) { + if (!token) { return { - response: createQoderErrorResponse({ - status: 400, - message: - "Qoder Personal Access Token is required. Connect Qoder with a PAT via qodercli transport.", - code: "pat_required", - }), - url: "qodercli://local", - headers, + response: new Response( + JSON.stringify({ + error: { + message: "Qoder access token or API Key is required. Please sign in or set a PAT.", + type: "authentication_error", + code: "token_required", + }, + }), + { status: 401, headers: { "Content-Type": "application/json" } } + ), + url: "https://dashscope.aliyuncs.com", + headers: { "Content-Type": "application/json" }, transformedBody: body, }; } - const prompt = buildQoderPrompt(body); - const workspace = getQoderCliWorkspace(); - const command = getQoderCliCommand(); + const resolvedModel = model || "qwen3-coder-plus"; - if (!stream) { - const result = await runQoderCliCommand({ - token: pat, - prompt, - stream: false, - model, - workspace, - command, + // Check if it's a model-alias matching QwenCode + let mappedModel = resolvedModel; + if (resolvedModel === "qwen3.5-plus" || resolvedModel === "qwen3.6-plus") { + mappedModel = "coder-model"; // Translate alias to what DashScope compatible endpoint accepts via QwenCode tokens + } else if (resolvedModel === "vision-model") { + mappedModel = "qwen3-vl-plus"; + } + + // Determine the resource URL: Qwen CLI tokens usually target portal.qwen.ai natively, + // but the DashScope compatible endpoint works out of the box when authtype is set. + // If the token was mapped to a custom `resource_url`, we should use it. Otherwise default to dashscope Aliyun. + let endpointUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; + + // We allow setting custom API base via credentials + let credentialsApiBase: unknown; + if (typeof credentials === "object" && credentials !== null) { + const credsObj = credentials as Record; + credentialsApiBase = credsObj.customApiBase || credsObj.resourceUrl; + } + if (typeof credentialsApiBase === "string" && credentialsApiBase.trim()) { + let base = credentialsApiBase.trim(); + if (!base.startsWith("http")) base = `https://${base}`; + if (!base.endsWith("/v1")) base = base.endsWith("/") ? `${base}v1` : `${base}/v1`; + endpointUrl = `${base}/chat/completions`; + } + + const headers: Record = { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "x-dashscope-authtype": "qwen-oauth", + "x-dashscope-cachecontrol": "enable", + "user-agent": "QwenCode/0.11.1 (linux; x64)", + "x-dashscope-useragent": "QwenCode/0.11.1 (linux; x64)", + "x-stainless-arch": "x64", + "x-stainless-lang": "js", + "x-stainless-os": "Linux", + }; + + mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders); + + const payload = { + ...(typeof body === "object" && body !== null ? body : {}), + model: mappedModel, + }; + + const bodyStr = JSON.stringify(payload); + + try { + const response = await fetch(endpointUrl, { + method: "POST", + headers, + body: bodyStr, signal, }); - if (!result.ok) { - const failure = parseQoderCliFailure(result.stderr || result.error || "", result.stdout); + const newHeaders = new Headers(response.headers); + + if (!response.ok) { + let errText = await response.text(); return { - response: createQoderErrorResponse(failure), - url: "qodercli://local", + response: new Response( + JSON.stringify({ + error: { + message: `Qoder API failed with status ${response.status}: ${errText}`, + type: response.status === 401 ? "authentication_error" : "provider_error", + }, + }), + { status: response.status, headers: { "Content-Type": "application/json" } } + ), + url: endpointUrl, headers, - transformedBody: body, + transformedBody: payload, }; } - let assistantText = result.stdout.trim(); - try { - const parsed = JSON.parse(assistantText); - assistantText = extractTextFromQoderEnvelope(parsed) || assistantText; - } catch { - // Fall back to raw stdout if the CLI printed plain text. - } - - const payload = buildQoderCompletionPayload({ - model, - text: assistantText, - }); - return { - response: new Response(JSON.stringify(payload), { - status: 200, - headers: { - "Content-Type": "application/json", - }, + response: new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, }), - url: "qodercli://local", + url: endpointUrl, headers, - transformedBody: body, + transformedBody: payload, + }; + } catch (e: unknown) { + const error = e as Error; + if (error.name === "AbortError") { + throw error; + } + return { + response: new Response( + JSON.stringify({ + error: { + message: `Qoder fetch error: ${error.message}`, + type: "provider_error", + }, + }), + { status: 502, headers: { "Content-Type": "application/json" } } + ), + url: endpointUrl, + headers, + transformedBody: payload, }; } - - const qoderStream = new ReadableStream({ - start: async (controller) => { - const encoder = new TextEncoder(); - const created = Math.floor(Date.now() / 1000); - const responseId = `chatcmpl-${crypto.randomUUID()}`; - const responseModel = model || "qoder-rome-30ba3b"; - const cliCommand = command; - const args = [ - "-q", - "-p", - prompt, - "--max-turns", - "1", - "--workspace", - workspace, - "--output-format", - "stream-json", - ]; - const level = mapQoderModelToLevel(responseModel); - if (level) { - args.push("--model", level); - } - - const child = spawn(cliCommand, args, { - env: { - ...process.env, - QODER_PERSONAL_ACCESS_TOKEN: pat, - }, - stdio: ["ignore", "pipe", "pipe"], - ...(process.platform === "win32" ? { shell: true } : {}), - }); - - let stdoutBuffer = ""; - let stderrBuffer = ""; - let emittedText = ""; - let roleSent = false; - let finished = false; - - const emitSse = (payload: unknown) => { - controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`)); - }; - - const finish = () => { - if (finished) return; - finished = true; - emitSse( - buildQoderChunk({ - id: responseId, - model: responseModel, - created, - delta: {}, - finishReason: "stop", - }) - ); - controller.enqueue(encoder.encode("data: [DONE]\n\n")); - controller.close(); - }; - - const abortChild = () => { - try { - child.kill("SIGTERM"); - } catch {} - }; - - if (signal?.aborted) { - abortChild(); - controller.error(new Error("aborted")); - return; - } - - signal?.addEventListener?.( - "abort", - () => { - abortChild(); - controller.error(new Error("aborted")); - }, - { once: true } - ); - - const emitDelta = (deltaText: string) => { - if (!deltaText) return; - const delta = roleSent - ? { content: deltaText } - : { role: "assistant", content: deltaText }; - roleSent = true; - emitSse( - buildQoderChunk({ - id: responseId, - model: responseModel, - created, - delta, - }) - ); - }; - - const drainStdout = () => { - let newlineIndex = stdoutBuffer.indexOf("\n"); - while (newlineIndex >= 0) { - const rawLine = stdoutBuffer.slice(0, newlineIndex).trim(); - stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1); - newlineIndex = stdoutBuffer.indexOf("\n"); - - if (!rawLine) continue; - - let parsed: unknown; - try { - parsed = JSON.parse(rawLine); - } catch { - continue; - } - - const nextText = extractTextFromQoderEnvelope(parsed); - if (nextText) { - const delta = nextText.startsWith(emittedText) - ? nextText.slice(emittedText.length) - : nextText; - emittedText += delta; - emitDelta(delta); - } - - const parsedRecord = - parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Record) - : {}; - if (parsedRecord.type === "result" && parsedRecord.done === true) { - finish(); - return; - } - } - }; - - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - - child.stdout.on("data", (chunk) => { - stdoutBuffer += chunk; - drainStdout(); - }); - - child.stderr.on("data", (chunk) => { - stderrBuffer += chunk; - }); - - child.on("error", (error) => { - if (finished) return; - controller.error(error); - }); - - child.on("close", (code) => { - if (finished) return; - drainStdout(); - if (code !== 0) { - const failure = parseQoderCliFailure(stderrBuffer, stdoutBuffer); - controller.error(new Error(failure.message)); - return; - } - finish(); - }); - }, - }); - - return { - response: new Response(qoderStream, { - status: 200, - headers: { - "Content-Type": "text/event-stream; charset=utf-8", - "Cache-Control": "no-cache, no-transform", - Connection: "keep-alive", - }, - }), - url: "qodercli://local", - headers, - transformedBody: body, - }; } } diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index ad216dfcbf..e04afe9904 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -23,7 +23,11 @@ import { formatProviderError, } from "../utils/error.ts"; import { HTTP_STATUS, PROVIDER_MAX_TOKENS } from "../config/constants.ts"; -import { classifyProviderError, PROVIDER_ERROR_TYPES } from "../services/errorClassifier.ts"; +import { + classifyProviderError, + PROVIDER_ERROR_TYPES, + isEmptyContentResponse, +} from "../services/errorClassifier.ts"; import { updateProviderConnection } from "@/lib/db/providers"; import { isDetailedLoggingEnabled } from "@/lib/db/detailedLogs"; import { logAuditEvent } from "@/lib/compliance"; @@ -82,7 +86,13 @@ import { } from "@/lib/semanticCache"; import { getIdempotencyKey, checkIdempotency, saveIdempotency } from "@/lib/idempotencyLayer"; import { createProgressTransform, wantsProgress } from "../utils/progressTracker.ts"; -import { isModelUnavailableError, getNextFamilyFallback } from "../services/modelFamilyFallback.ts"; +import { + isModelUnavailableError, + getNextFamilyFallback, + isContextOverflowError, + findLargerContextModel, + getModelFamily, +} from "../services/modelFamilyFallback.ts"; import { computeRequestHash, deduplicate, shouldDeduplicate } from "../services/requestDedup.ts"; import { getBackgroundTaskReason, @@ -1341,8 +1351,7 @@ export async function handleChatCore({ provider === "qwen" && parsedStatusCode === HTTP_STATUS.BAD_REQUEST && parsedMessage && - (parsedMessage.toLowerCase().includes("session has expired") || - parsedMessage.toLowerCase().includes("invalid_parameter_error")); + parsedMessage.toLowerCase().includes("session has expired"); const streamOptionsOnlyFailed = false; // TODO: properly track stream options failure? (placeholder from existing logic) @@ -1527,6 +1536,16 @@ export async function handleChatCore({ lastError: message, errorCode: statusCode, }); + } else if (errorType === PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN) { + // OAuth 401 with invalid credentials - token refresh can recover + await updateProviderConnection(connectionId, { + lastErrorType: errorType, + lastError: message, + errorCode: statusCode, + }); + console.warn( + `[provider] Node ${connectionId} OAuth token invalid (${statusCode}) — token refresh available` + ); } else if (errorType === PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR) { // Cloud Code 403 with stale project: not a ban, keep account active. await updateProviderConnection(connectionId, { @@ -1626,6 +1645,63 @@ export async function handleChatCore({ persistFailureUsage(statusCode, "model_unavailable"); return createErrorResult(statusCode, errMsg, retryAfterMs); } + } else if (isContextOverflowError(statusCode, message)) { + const familyCandidates = getModelFamily(currentModel).filter( + (m) => m !== currentModel && !triedModels.has(m) + ); + const nextModel = + findLargerContextModel(currentModel, familyCandidates) ?? + getNextFamilyFallback(currentModel, triedModels); + if (nextModel) { + triedModels.add(nextModel); + currentModel = nextModel; + translatedBody.model = nextModel; + log?.info?.("CONTEXT_OVERFLOW_FALLBACK", `${model} context overflow → trying ${nextModel}`); + try { + const fallbackResult = await executeProviderRequest(nextModel, false); + if (fallbackResult.response.ok) { + providerResponse = fallbackResult.response; + providerUrl = fallbackResult.url; + providerHeaders = fallbackResult.headers; + finalBody = fallbackResult.transformedBody; + reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody); + log?.info?.( + "CONTEXT_OVERFLOW_FALLBACK", + `Serving ${nextModel} as fallback for ${model}` + ); + } else { + persistAttemptLogs({ + status: statusCode, + error: errMsg, + providerRequest: finalBody || translatedBody, + providerResponse: upstreamErrorBody, + clientResponse: buildErrorBody(statusCode, errMsg), + }); + persistFailureUsage(statusCode, "context_overflow"); + return createErrorResult(statusCode, errMsg, retryAfterMs); + } + } catch { + persistAttemptLogs({ + status: statusCode, + error: errMsg, + providerRequest: finalBody || translatedBody, + providerResponse: upstreamErrorBody, + clientResponse: buildErrorBody(statusCode, errMsg), + }); + persistFailureUsage(statusCode, "context_overflow"); + return createErrorResult(statusCode, errMsg, retryAfterMs); + } + } else { + persistAttemptLogs({ + status: statusCode, + error: errMsg, + providerRequest: finalBody || translatedBody, + providerResponse: upstreamErrorBody, + clientResponse: buildErrorBody(statusCode, errMsg), + }); + persistFailureUsage(statusCode, "context_overflow"); + return createErrorResult(statusCode, errMsg, retryAfterMs); + } } else { persistAttemptLogs({ status: statusCode, @@ -1764,6 +1840,63 @@ export async function handleChatCore({ } } + // Check for empty content response (fake success) - trigger fallback + if (isEmptyContentResponse(responseBody)) { + appendRequestLog({ + model, + provider, + connectionId, + status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}`, + }).catch(() => {}); + const emptyContentMessage = "Provider returned empty content"; + persistAttemptLogs({ + status: HTTP_STATUS.BAD_GATEWAY, + error: emptyContentMessage, + providerRequest: finalBody || translatedBody, + providerResponse: normalizedProviderPayload, + clientResponse: buildErrorBody(HTTP_STATUS.BAD_GATEWAY, emptyContentMessage), + }); + persistFailureUsage(HTTP_STATUS.BAD_GATEWAY, "empty_content"); + + // Trigger non-recursive fallback for empty content + const nextModel = getNextFamilyFallback(currentModel, triedModels); + if (nextModel) { + triedModels.add(nextModel); + currentModel = nextModel; + translatedBody.model = nextModel; + log?.info?.( + "EMPTY_CONTENT_FALLBACK", + `${model} returned empty content → trying ${nextModel}` + ); + try { + const fallbackResult = await executeProviderRequest(nextModel, false); + if (fallbackResult.response.ok) { + const fallbackRaw = await fallbackResult.response.text(); + try { + responseBody = fallbackRaw ? JSON.parse(fallbackRaw) : {}; + providerUrl = fallbackResult.url; + providerHeaders = fallbackResult.headers; + finalBody = fallbackResult.transformedBody; + reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody); + log?.info?.( + "EMPTY_CONTENT_FALLBACK", + `Serving ${nextModel} as fallback for ${model}` + ); + // Fall through — continue processing with the new responseBody + } catch { + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, emptyContentMessage); + } + } else { + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, emptyContentMessage); + } + } catch { + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, emptyContentMessage); + } + } else { + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, emptyContentMessage); + } + } + if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE) { responseBody = restoreClaudePassthroughToolNames(responseBody, toolNameMap); } diff --git a/open-sse/package.json b/open-sse/package.json index a12ff92fcf..92a132179b 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.5.1", + "version": "3.5.2", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts index d0c09e845e..8e8f75236c 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -36,6 +36,15 @@ export const CREDITS_EXHAUSTED_SIGNALS = [ "payment required", ]; +// T11: Signals that indicate OAuth token is invalid/expired (not permanent deactivation) +export const OAUTH_INVALID_TOKEN_SIGNALS = [ + "invalid authentication credentials", + "oauth 2", + "login cookie", + "valid authentication credential", + "invalid credentials", +]; + /** * T06: Returns true if response body indicates the account is permanently deactivated. */ @@ -52,6 +61,15 @@ export function isCreditsExhausted(errorText: string): boolean { return CREDITS_EXHAUSTED_SIGNALS.some((sig) => lower.includes(sig)); } +/** + * T11: Returns true if response body indicates OAuth token is invalid/expired. + * This is different from permanent account deactivation - token refresh can recover. + */ +export function isOAuthInvalidToken(errorText: string): boolean { + const lower = String(errorText || "").toLowerCase(); + return OAUTH_INVALID_TOKEN_SIGNALS.some((sig) => lower.includes(sig)); +} + // ─── Provider Profile Helper ──────────────────────────────────────────────── /** diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 85c02db5e5..0294bf3199 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -172,11 +172,9 @@ export function buildClaudeCodeCompatibleRequest({ const effort = resolveClaudeCodeCompatibleEffort(sourceBody, normalizedBody, model); const maxTokens = resolveClaudeCodeCompatibleMaxTokens(sourceBody, normalizedBody); const tools = preparedClaudeBody?.tools - ? applyClaudeCodeCompatibleToolCacheStrategy( + ? buildClaudeCodeCompatibleToolsFromClaude( preparedClaudeBody.tools as Record[], - { - preserveExisting: preserveCacheControl, - } + preserveCacheControl ) : buildClaudeCodeCompatibleTools(normalizedBody, sourceBody); const toolChoice = @@ -312,14 +310,12 @@ function buildClaudeCodeCompatibleMessages(messages: MessageLike[]) { return [ { role: "user" as const, - content: [{ type: "text", text: fallbackText, cache_control: { type: "ephemeral" } }], + content: [{ type: "text", text: fallbackText }], }, ]; } } - applyClaudeCodeCompatibleMessageCacheStrategy(merged); - return merged; } @@ -358,9 +354,6 @@ function buildClaudeCodeCompatibleMessagesFromClaude( stripCacheControlFromContentBlocks(message.content); } } - applyClaudeCodeCompatibleMessageCacheStrategy(merged, { - preserveExisting: preserveCacheControl, - }); if (merged.length === 0) { const fallbackText = converted @@ -373,7 +366,7 @@ function buildClaudeCodeCompatibleMessagesFromClaude( return [ { role: "user" as const, - content: [{ type: "text", text: fallbackText, cache_control: { type: "ephemeral" } }], + content: [{ type: "text", text: fallbackText }], }, ]; } @@ -399,12 +392,6 @@ function buildClaudeCodeCompatibleSystemBlocks({ Array.isArray(systemBlocks) && systemBlocks.length > 0 ? systemBlocks.map((block) => ({ ...block })) : extractCustomSystemBlocks(messages); - const useLongSystemTtl = - !preserveCacheControl || - customSystemBlocks.some((block) => readCacheControlTtl(block) === "1h"); - const systemCacheControl = useLongSystemTtl - ? { type: "ephemeral", ttl: "1h" } - : { type: "ephemeral" }; const dateText = formatDate(now); const blocks: Array> = [ @@ -415,32 +402,21 @@ function buildClaudeCodeCompatibleSystemBlocks({ { type: "text", text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.", - cache_control: { ...systemCacheControl }, }, { type: "text", text: `You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${cwd}\nDate: ${dateText}`, - cache_control: { ...systemCacheControl }, }, ]; for (const systemBlock of customSystemBlocks) { const preparedBlock = { ...systemBlock }; - if (!preserveCacheControl || useLongSystemTtl) { - preparedBlock.cache_control = { ...systemCacheControl }; + if (!preserveCacheControl) { + delete preparedBlock.cache_control; } blocks.push(preparedBlock); } - if ( - preserveCacheControl && - customSystemBlocks.length > 0 && - !customSystemBlocks.some((block) => hasCacheControl(block)) - ) { - const lastCustomSystemBlock = blocks[blocks.length - 1]; - lastCustomSystemBlock.cache_control = { ...systemCacheControl }; - } - return blocks; } @@ -477,11 +453,22 @@ function buildClaudeCodeCompatibleTools( return rawTools .map((tool) => convertClaudeCodeCompatibleTool(tool)) .filter((tool): tool is Record => !!tool) - .map((tool) => ({ ...tool })) - .map((tool, index, tools) => { - if (index !== findLastCacheableToolIndex(tools)) return tool; - return { ...tool, cache_control: { type: "ephemeral", ttl: "1h" } }; - }); + .map((tool) => ({ ...tool })); +} + +function buildClaudeCodeCompatibleToolsFromClaude( + tools: Record[] | undefined, + preserveCacheControl: boolean +) { + if (!Array.isArray(tools)) return []; + + return tools.map((tool) => { + const preparedTool = { ...tool }; + if (!preserveCacheControl) { + delete preparedTool.cache_control; + } + return preparedTool; + }); } function convertClaudeCodeCompatibleTool(tool: unknown) { @@ -546,6 +533,7 @@ function prepareClaudeCodeCompatibleBody( claudeBody: Record, preserveCacheControl: boolean ) { + void preserveCacheControl; const prepared = prepareClaudeRequest( { system: normalizeClaudeSystemInput(claudeBody.system), @@ -554,7 +542,7 @@ function prepareClaudeCodeCompatibleBody( thinking: readRecord(claudeBody.thinking) || claudeBody.thinking, }, CLAUDE_CODE_COMPATIBLE_PREFIX, - preserveCacheControl + true ); return readRecord(prepared); @@ -673,111 +661,12 @@ function extractCustomSystemBlocks(messages: MessageLike[] | undefined) { })); } -function applyClaudeCodeCompatibleMessageCacheStrategy( - messages: Array<{ role: "user" | "assistant"; content: Array> }>, - options: { - preserveExisting?: boolean; - } = {} -) { - const preserveExisting = options.preserveExisting === true; - const userIndexes = messages.reduce((indexes, message, index) => { - if (message.role === "user") indexes.push(index); - return indexes; - }, [] as number[]); - const hasAssistant = messages.some((message) => message.role === "assistant"); - const secondToLastUserIndex = userIndexes.length >= 2 ? userIndexes[userIndexes.length - 2] : -1; - - if (secondToLastUserIndex >= 0) { - markLastContentCacheControl( - messages[secondToLastUserIndex].content, - undefined, - preserveExisting - ); - } else if (!hasAssistant && userIndexes.length > 0) { - markLastContentCacheControl( - messages[userIndexes[userIndexes.length - 1]].content, - undefined, - preserveExisting - ); - } - - const lastUserIndex = userIndexes.length > 0 ? userIndexes[userIndexes.length - 1] : -1; - const endsOnUser = lastUserIndex === messages.length - 1; - if (endsOnUser && lastUserIndex >= 0) { - markLastContentCacheControl(messages[lastUserIndex].content, undefined, preserveExisting); - } - - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role !== "assistant") continue; - if (markLastContentCacheControl(messages[i].content, undefined, preserveExisting)) break; - } -} - function stripCacheControlFromContentBlocks(content: Array>) { for (const block of content) { delete block.cache_control; } } -function applyClaudeCodeCompatibleToolCacheStrategy( - tools: Array>, - options: { - preserveExisting?: boolean; - } = {} -) { - const preparedTools = tools.map((tool) => ({ ...tool })); - const lastCacheableToolIndex = findLastCacheableToolIndex(preparedTools); - if (lastCacheableToolIndex < 0) return preparedTools; - - if (options.preserveExisting && preparedTools.some((tool) => hasCacheControl(tool))) { - return preparedTools; - } - - preparedTools[lastCacheableToolIndex].cache_control = { type: "ephemeral", ttl: "1h" }; - return preparedTools; -} - -function markLastContentCacheControl( - content: Array>, - ttl?: string, - preserveExisting = false -) { - if (!Array.isArray(content) || content.length === 0) return false; - const lastBlock = content[content.length - 1]; - if (!lastBlock) return false; - if ( - preserveExisting && - content.some( - (block) => - !!block && - typeof block === "object" && - !!readRecord(block)?.cache_control && - typeof readRecord(block)?.cache_control === "object" - ) - ) { - return true; - } - lastBlock.cache_control = ttl ? { type: "ephemeral", ttl } : { type: "ephemeral" }; - return true; -} - -function hasCacheControl(value: unknown) { - return !!readRecord(value)?.cache_control && typeof readRecord(value)?.cache_control === "object"; -} - -function readCacheControlTtl(value: unknown) { - return toNonEmptyString(readRecord(readRecord(value)?.cache_control)?.ttl); -} - -function findLastCacheableToolIndex(tools: Array>) { - for (let i = tools.length - 1; i >= 0; i--) { - if (!tools[i].defer_loading) { - return i; - } - } - return -1; -} - function cloneValue(value: T): T { if (typeof structuredClone === "function") { return structuredClone(value); diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index e6ba23712b..7f86b8e9f4 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -17,6 +17,7 @@ import { selectProvider as selectAutoProvider } from "./autoCombo/engine.ts"; import { selectWithStrategy } from "./autoCombo/routerStrategy.ts"; import { DEFAULT_WEIGHTS, scorePool } from "./autoCombo/scoring.ts"; import { supportsToolCalling } from "./modelCapabilities.ts"; +import { getModelContextLimit } from "../../src/lib/modelsDevSync"; // Status codes that should mark semaphore + record circuit breaker failures const TRANSIENT_FOR_BREAKER = [429, 502, 503, 504]; @@ -309,6 +310,24 @@ function sortModelsByUsage(models, comboName) { return withUsage.map((e) => e.modelStr); } +/** + * Sort models by context window size (largest first) for context-optimized strategy. + * Uses models.dev synced capabilities to get context limits. + * @param {Array} models - Model strings in "provider/model" format + * @returns {Array} Sorted model strings (largest context first) + */ +function sortModelsByContextSize(models) { + const withContext = models.map((modelStr) => { + const parsed = parseModel(modelStr); + const provider = parsed.provider || parsed.providerAlias || "unknown"; + const model = parsed.model || modelStr; + const limit = getModelContextLimit(provider, model); + return { modelStr, context: limit ?? 0 }; + }); + withContext.sort((a, b) => b.context - a.context); + return withContext.map((e) => e.modelStr); +} + function toTextContent(content) { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; @@ -908,6 +927,9 @@ export async function handleComboChat({ } else if (strategy === "cost-optimized") { orderedModels = await sortModelsByCost(orderedModels); log.info("COMBO", `Cost-optimized ordering: cheapest first (${orderedModels[0]})`); + } else if (strategy === "context-optimized") { + orderedModels = sortModelsByContextSize(orderedModels); + log.info("COMBO", `Context-optimized ordering: largest first (${orderedModels[0]})`); } let lastError = null; diff --git a/open-sse/services/contextManager.ts b/open-sse/services/contextManager.ts index 194a252091..a899a141b5 100644 --- a/open-sse/services/contextManager.ts +++ b/open-sse/services/contextManager.ts @@ -6,6 +6,7 @@ */ import { REGISTRY } from "../config/providerRegistry.ts"; +import { getModelContextLimit } from "../../src/lib/modelsDevSync"; // Default token limits per provider (fallbacks when not in registry) const DEFAULT_LIMITS: Record = { @@ -47,20 +48,26 @@ export function estimateTokens(text) { /** * Get token limit for a provider/model combination - * Priority: Env override > Registry defaultContextLength > DEFAULT_LIMITS + * Priority: Env override > models.dev DB > Registry defaultContextLength > DEFAULT_LIMITS */ export function getTokenLimit(provider, model = null) { // 1. Check environment variable override first const envOverride = getEnvOverride(provider); if (envOverride) return envOverride; - // 2. Check registry for provider default + // 2. Check models.dev synced DB for per-model context limit + if (model) { + const dbLimit = getModelContextLimit(provider, model); + if (dbLimit && dbLimit > 0) return dbLimit; + } + + // 3. Check registry for provider default const registryEntry = REGISTRY[provider]; if (registryEntry?.defaultContextLength) { return registryEntry.defaultContextLength; } - // 3. Check if model name hints at a known limit + // 4. Check if model name hints at a known limit if (model) { const lower = model.toLowerCase(); if (lower.includes("claude")) return DEFAULT_LIMITS.claude; @@ -75,7 +82,7 @@ export function getTokenLimit(provider, model = null) { return DEFAULT_LIMITS.codex; } - // 4. Fallback to DEFAULT_LIMITS or default + // 5. Fallback to DEFAULT_LIMITS or default return DEFAULT_LIMITS[provider] || DEFAULT_LIMITS.default; } diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index 4d6444c91c..fc03f1f684 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -1,15 +1,79 @@ -import { isAccountDeactivated, isCreditsExhausted } from "./accountFallback.ts"; +import { + isAccountDeactivated, + isCreditsExhausted, + isOAuthInvalidToken, +} from "./accountFallback.ts"; + +export function isEmptyContentResponse(responseBody: unknown): boolean { + if (!responseBody || typeof responseBody !== "object") return false; + + const body = responseBody as Record; + + if (Array.isArray(body.choices)) { + const firstChoice = body.choices[0] as Record | undefined; + if (!firstChoice) return true; + + const message = firstChoice.message as Record | undefined; + const delta = firstChoice.delta as Record | undefined; + + const content = message?.content ?? delta?.content; + const hasToolCalls = + (Array.isArray(message?.tool_calls) && (message.tool_calls as unknown[]).length > 0) || + (Array.isArray(delta?.tool_calls) && (delta.tool_calls as unknown[]).length > 0); + + const hasContent = content !== null && content !== undefined && content !== ""; + return !hasContent && !hasToolCalls; + } + + if (Array.isArray(body.content)) { + return body.content.length === 0; + } + + if (typeof body.text === "string") { + return body.text.trim() === ""; + } + + if ("content" in body) { + const content = body.content; + return content === null || content === undefined || content === ""; + } + + return false; +} export const PROVIDER_ERROR_TYPES = { - RATE_LIMITED: "rate_limited", // 429 — transient, retry with backoff - UNAUTHORIZED: "unauthorized", // 401 — token expired, refresh - ACCOUNT_DEACTIVATED: "account_deactivated", // 401 + deactivation signal - FORBIDDEN: "forbidden", // 403 — account banned/revoked, disable node - SERVER_ERROR: "server_error", // 500/502/503 — retry limited - QUOTA_EXHAUSTED: "quota_exhausted", // 402/429/400 + billing signals - PROJECT_ROUTE_ERROR: "project_route_error", // 403 + stale project — transient, not a ban + RATE_LIMITED: "rate_limited", + UNAUTHORIZED: "unauthorized", + ACCOUNT_DEACTIVATED: "account_deactivated", + FORBIDDEN: "forbidden", + SERVER_ERROR: "server_error", + QUOTA_EXHAUSTED: "quota_exhausted", + PROJECT_ROUTE_ERROR: "project_route_error", + CONTEXT_OVERFLOW: "context_overflow", + OAUTH_INVALID_TOKEN: "oauth_invalid_token", + EMPTY_CONTENT: "empty_content", }; +export const CONTEXT_OVERFLOW_SIGNALS = [ + "context overflow", + "prompt too large", + "context window", + "maximum context", + "exceeds context", + "input too long", + "token limit", + "too many tokens", + "context length", + "exceed.*context", + "messages exceed", +]; + +export const CONTEXT_OVERFLOW_REGEX = new RegExp(CONTEXT_OVERFLOW_SIGNALS.join("|"), "i"); + +export function isContextOverflow(errorText: string): boolean { + return CONTEXT_OVERFLOW_REGEX.test(String(errorText || "")); +} + function responseBodyToString(responseBody: unknown): string { if (typeof responseBody === "string") return responseBody; if (responseBody !== null && typeof responseBody === "object") { @@ -26,8 +90,8 @@ export function classifyProviderError(statusCode: number, responseBody: unknown) const bodyStr = responseBodyToString(responseBody); const creditsExhausted = isCreditsExhausted(bodyStr); const accountDeactivated = isAccountDeactivated(bodyStr); + const oauthInvalid = isOAuthInvalidToken(bodyStr); - // T10: credits exhausted is terminal and can appear as 400/402/429 depending on provider. if ( creditsExhausted && (statusCode === 400 || statusCode === 402 || statusCode === 429 || statusCode === 403) @@ -39,8 +103,10 @@ export function classifyProviderError(statusCode: number, responseBody: unknown) return PROVIDER_ERROR_TYPES.RATE_LIMITED; } - // T06: only deactivation-like 401s should be treated as permanent account expiry. if (statusCode === 401) { + if (oauthInvalid) { + return PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN; + } return accountDeactivated ? PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED : PROVIDER_ERROR_TYPES.UNAUTHORIZED; @@ -51,9 +117,6 @@ export function classifyProviderError(statusCode: number, responseBody: unknown) return PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED; } if (statusCode === 403) { - // Cloud Code API returns 403 with "has not been used in project X" when the project - // field is wrong or stale. This is a routing/config error, not an account ban. - // Classify as project_route_error so the account stays active but the error is tracked. if (bodyStr.includes("has not been used in project")) { return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR; } @@ -61,5 +124,9 @@ export function classifyProviderError(statusCode: number, responseBody: unknown) } if (statusCode >= 500) return PROVIDER_ERROR_TYPES.SERVER_ERROR; + if (statusCode === 400 && isContextOverflow(bodyStr)) { + return PROVIDER_ERROR_TYPES.CONTEXT_OVERFLOW; + } + return null; } diff --git a/open-sse/services/modelFamilyFallback.ts b/open-sse/services/modelFamilyFallback.ts index dc8a2bfbc2..73e25f32db 100644 --- a/open-sse/services/modelFamilyFallback.ts +++ b/open-sse/services/modelFamilyFallback.ts @@ -11,6 +11,10 @@ * (commit 6cea566, Mar 8 2026). */ +import { getModelContextLimit } from "../../src/lib/modelsDevSync"; +import { parseModel } from "./model.ts"; +import { CONTEXT_OVERFLOW_REGEX } from "./errorClassifier.ts"; + // ── Model Family Definitions ───────────────────────────────────────────────── /** @@ -116,6 +120,11 @@ export function isModelUnavailableError(status: number, errorMessage: string): b return MODEL_UNAVAILABLE_FRAGMENTS.some((fragment) => msg.includes(fragment)); } +export function isContextOverflowError(status: number, errorMessage: string): boolean { + if (status !== 400) return false; + return CONTEXT_OVERFLOW_REGEX.test(errorMessage); +} + // ── Fallback Resolution ────────────────────────────────────────────────────── /** @@ -156,3 +165,35 @@ export function getModelFamily(model: string): string[] { if (!family) return [model]; return [model, ...family]; } + +/** + * Find a model with larger context window from a list of candidate models. + * Uses models.dev synced capabilities to compare context limits. + */ +export function findLargerContextModel( + currentModel: string, + availableModels: string[] +): string | null { + const currentParsed = parseModel(currentModel); + const currentProvider = currentParsed.provider || currentParsed.providerAlias || "unknown"; + const currentModelId = currentParsed.model || currentModel; + const currentLimit = getModelContextLimit(currentProvider, currentModelId) ?? 0; + + let bestModel: string | null = null; + let bestLimit = currentLimit; + + for (const candidate of availableModels) { + if (candidate === currentModel) continue; + const parsed = parseModel(candidate); + const provider = parsed.provider || parsed.providerAlias || "unknown"; + const modelId = parsed.model || candidate; + const limit = getModelContextLimit(provider, modelId) ?? 0; + + if (limit > bestLimit) { + bestLimit = limit; + bestModel = candidate; + } + } + + return bestModel; +} diff --git a/open-sse/services/qoderCli.ts b/open-sse/services/qoderCli.ts index 327b83382d..ca8e7be1a2 100644 --- a/open-sse/services/qoderCli.ts +++ b/open-sse/services/qoderCli.ts @@ -304,7 +304,7 @@ export function buildQoderChunk({ export function parseQoderCliFailure(stderrText: string, stdoutText = ""): QoderCliFailure { const stderr = String(stderrText || "").trim(); const stdout = String(stdoutText || "").trim(); - const combined = `${stderr}\n${stdout}`.trim() || "Qoder CLI request failed"; + const combined = `${stderr}\n${stdout}`.trim() || "Qoder API request failed"; const normalized = combined.toLowerCase(); if ( @@ -316,15 +316,6 @@ export function parseQoderCliFailure(stderrText: string, stdoutText = ""): Qoder return { status: 401, message: combined, code: "upstream_auth_error" }; } - if ( - normalized.includes("command not found") || - normalized.includes("not installed") || - normalized.includes("enoent") || - normalized.includes("no such file or directory") - ) { - return { status: 503, message: combined, code: "runtime_error" }; - } - if (normalized.includes("timed out") || normalized.includes("timeout")) { return { status: 504, message: combined, code: "timeout" }; } @@ -350,123 +341,56 @@ export function createQoderErrorResponse(failure: QoderCliFailure): Response { ); } -export async function runQoderCliCommand({ - token, - prompt, - stream, - model, - workspace, - command, - signal, - timeoutMs = DEFAULT_TIMEOUT_MS, -}: QoderCliRunOptions): Promise { - const cliCommand = String(command || getQoderCliCommand()).trim(); - const cwd = String(workspace || getQoderCliWorkspace()).trim() || process.cwd(); - const args = [ - "-q", - "-p", - prompt, - "--max-turns", - DEFAULT_MAX_TURNS, - "--workspace", - cwd, - "--output-format", - stream ? "stream-json" : "json", - ]; +const PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc +4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l +6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17 +XcW+ML9FoCI6AOvOzwIDAQAB +-----END PUBLIC KEY-----`; - const level = mapQoderModelToLevel(model || QODER_DEFAULT_MODEL); - if (level) { - args.push("--model", level); - } +function buildCosyHeadersForValidation(bodyStr: string, token: string) { + const aesKeyBytes = crypto.randomBytes(16); + const aesKeyStr = aesKeyBytes.toString("hex").slice(0, 16); + const aesKeyBuf = Buffer.from(aesKeyStr, "utf8"); - return new Promise((resolve) => { - let stdout = ""; - let stderr = ""; - let timedOut = false; - let settled = false; + const uid = "omniroute.user@qoder.sh"; + const userInfo = { + uid: uid, + security_oauth_token: token, + name: "omniroute", + aid: "", + email: uid, + }; - const child = spawn(cliCommand, args, { - env: { - ...process.env, - QODER_PERSONAL_ACCESS_TOKEN: token, - }, - stdio: ["ignore", "pipe", "pipe"], - ...(process.platform === "win32" ? { shell: true } : {}), - }); + const cipher = crypto.createCipheriv("aes-128-cbc", aesKeyBuf, aesKeyBuf); + let ciphertext = cipher.update(JSON.stringify(userInfo), "utf8", "base64"); + ciphertext += cipher.final("base64"); - const cleanup = (result: QoderCliRunResult) => { - if (settled) return; - settled = true; - clearTimeout(timer); - signal?.removeEventListener?.("abort", abortHandler); - resolve(result); - }; - - const abortChild = () => { - try { - child.kill("SIGTERM"); - } catch {} - setTimeout(() => { - try { - child.kill("SIGKILL"); - } catch {} - }, 250).unref?.(); - }; - - const abortHandler = () => { - abortChild(); - cleanup({ - ok: false, - code: null, - stdout: stdout.trim(), - stderr: stderr.trim(), - timedOut: false, - error: "aborted", - }); - }; - - if (signal?.aborted) { - abortHandler(); - return; - } - - signal?.addEventListener?.("abort", abortHandler, { once: true }); - - const timer = setTimeout(() => { - timedOut = true; - abortChild(); - }, timeoutMs); - - child.stdout.on("data", (chunk) => { - stdout += chunk.toString(); - }); - - child.stderr.on("data", (chunk) => { - stderr += chunk.toString(); - }); - - child.on("error", (error) => { - cleanup({ - ok: false, - code: null, - stdout: stdout.trim(), - stderr: stderr.trim(), - timedOut, - error: error?.message || "spawn_error", - }); - }); - - child.on("close", (code) => { - cleanup({ - ok: !timedOut && code === 0, - code, - stdout: stdout.trim(), - stderr: stderr.trim(), - timedOut, - error: timedOut ? "timeout" : null, - }); - }); + const encryptedKeyBuf = crypto.publicEncrypt( + { key: PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_PADDING }, + aesKeyBuf + ); + const cosyKeyB64 = encryptedKeyBuf.toString("base64"); + const timestamp = Math.floor(Date.now() / 1000).toString(); + const payloadStr = JSON.stringify({ + version: "v1", + requestId: crypto.randomUUID(), + info: ciphertext, + cosyVersion: "0.12.3", + ideVersion: "", }); + const payloadB64 = Buffer.from(payloadStr).toString("base64"); + const sigPath = "/api/v2/service/pro/sse/agent_chat_generation"; + const sigInput = `${payloadB64}\n${cosyKeyB64}\n${timestamp}\n${bodyStr}\n${sigPath}`; + const sig = crypto.createHash("md5").update(sigInput).digest("hex"); + + return { + Authorization: `Bearer COSY.${payloadB64}.${sig}`, + "Cosy-Key": cosyKeyB64, + "Cosy-User": uid, + "Cosy-Date": timestamp, + "Content-Type": "application/json", + }; } export async function validateQoderCliPat({ @@ -478,24 +402,32 @@ export async function validateQoderCliPat({ }) { const modelId = getString(providerSpecificData.validationModelId).trim() || - getString(providerSpecificData.modelId).trim(); - const result = await runQoderCliCommand({ - token: apiKey, - prompt: "Reply with OK only.", + getString(providerSpecificData.modelId).trim() || + QODER_DEFAULT_MODEL; + + const bodyStr = JSON.stringify({ + model: modelId || "coder-model", + messages: [{ role: "user", content: "hi" }], stream: false, - model: modelId || QODER_DEFAULT_MODEL, - workspace: getQoderCliWorkspace(), - timeoutMs: 30_000, }); - if (!result.ok) { - const failure = parseQoderCliFailure(result.stderr || result.error || "", result.stdout); - return { - valid: false, - error: failure.status === 401 ? "Invalid API key" : failure.message, - unsupported: false, - }; - } + const headers = buildCosyHeadersForValidation(bodyStr, apiKey); + const endpoint = + "https://api1.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?AgentId=agent_common"; - return { valid: true, error: null, unsupported: false }; + try { + const res = await fetch(endpoint, { + method: "POST", + headers, + body: bodyStr, + // @ts-ignore + signal: AbortSignal.timeout(30000), + }); + if (!res.ok) { + return { valid: false, error: `HTTP ${res.status}: ${await res.text()}`, unsupported: false }; + } + return { valid: true, error: null, unsupported: false }; + } catch (e: any) { + return { valid: false, error: e.message, unsupported: false }; + } } diff --git a/open-sse/utils/cacheControlPolicy.ts b/open-sse/utils/cacheControlPolicy.ts index cd538e2ebf..0810674bb1 100644 --- a/open-sse/utils/cacheControlPolicy.ts +++ b/open-sse/utils/cacheControlPolicy.ts @@ -148,13 +148,6 @@ export function shouldPreserveCacheControl({ return false; } - // CC-compatible bridges should default to OmniRoute-managed cache markers. - // Their request shape differs from native Claude Messages payloads, so - // preserving client markers in auto mode weakens cache coverage. - if (typeof targetProvider === "string" && targetProvider.startsWith("anthropic-compatible-cc-")) { - return false; - } - // Auto mode: use automatic detection (existing logic) // Must be a caching-aware client if (!isClaudeCodeClient(userAgent)) { diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 56ae32857e..502e256549 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -595,8 +595,12 @@ export function createSSEStream(options: StreamOptions = {}) { // When translating Responses -> Claude, `item` is already a Claude SSE event; // sanitizing it as an OpenAI chunk strips message_start/content_block_delta/message_stop // and causes Claude Code to drop the assistant message. + // #761: Responses API events have {event, data} structure — skip sanitization + // entirely as it strips them to {"object":"chat.completion.chunk"}, losing all content. let itemSanitized: Record = item; - if (sourceFormat === FORMATS.OPENAI || sourceFormat === FORMATS.OPENAI_RESPONSES) { + const isResponsesEvent = + typeof item?.event === "string" && item.event.startsWith("response."); + if (sourceFormat === FORMATS.OPENAI && !isResponsesEvent) { itemSanitized = sanitizeStreamingChunk(itemSanitized) as Record; // Extract reasoning tags from content if translation generated them diff --git a/package-lock.json b/package-lock.json index ec68047fc5..2b1333ac53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.5.1", + "version": "3.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.5.1", + "version": "3.5.2", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -29,7 +29,7 @@ "js-yaml": "^4.1.0", "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", - "next": "^16.0.10", + "next": "16.0.10", "next-intl": "^4.8.3", "node-machine-id": "^1.1.12", "open": "^11.0.0", @@ -68,7 +68,7 @@ "concurrently": "^9.2.1", "cross-env": "^10.1.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.0.10", + "eslint-config-next": "16.0.10", "husky": "^9.1.7", "jsdom": "^29.0.1", "lint-staged": "^16.2.7", @@ -2057,9 +2057,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2076,9 +2073,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2095,9 +2089,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2114,9 +2105,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2133,9 +2121,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2152,9 +2137,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2171,9 +2153,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2190,9 +2169,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2209,9 +2185,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2234,9 +2207,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2259,9 +2229,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2284,9 +2251,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2309,9 +2273,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2334,9 +2295,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2359,9 +2317,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2384,9 +2339,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2844,15 +2796,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", - "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", + "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.2.tgz", - "integrity": "sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.10.tgz", + "integrity": "sha512-b2NlWN70bbPLmfyoLvvidPKWENBYYIe017ZGUpElvQjDytCWgxPJx7L9juxHt0xHvNVA08ZHJdOyhGzon/KJuw==", "dev": true, "license": "MIT", "dependencies": { @@ -2860,9 +2812,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", - "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz", + "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==", "cpu": [ "arm64" ], @@ -2876,9 +2828,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", - "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz", + "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==", "cpu": [ "x64" ], @@ -2892,15 +2844,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", - "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz", + "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==", "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2911,15 +2860,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", - "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz", + "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2930,15 +2876,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", - "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz", + "integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==", "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2949,15 +2892,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", - "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz", + "integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2968,9 +2908,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", - "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz", + "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==", "cpu": [ "arm64" ], @@ -2984,9 +2924,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", - "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", + "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", "cpu": [ "x64" ], @@ -3183,9 +3123,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3206,9 +3143,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3229,9 +3163,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3252,9 +3183,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3275,9 +3203,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3298,9 +3223,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5139,9 +5061,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5159,9 +5078,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5179,9 +5095,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5199,9 +5112,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5219,9 +5129,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5239,9 +5146,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5570,9 +5474,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5589,9 +5490,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5608,9 +5506,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5627,9 +5522,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5646,9 +5538,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5665,9 +5554,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5882,9 +5768,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5902,9 +5785,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5922,9 +5802,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5942,9 +5819,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7020,9 +6894,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7037,9 +6908,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7054,9 +6922,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7071,9 +6936,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7088,9 +6950,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7105,9 +6964,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7122,9 +6978,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7139,9 +6992,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7978,6 +7828,7 @@ "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==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -10245,13 +10096,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.2.tgz", - "integrity": "sha512-6VlvEhwoug2JpVgjZDhyXrJXUEuPY++TddzIpTaIRvlvlXXFgvQUtm3+Zr84IjFm0lXtJt73w19JA08tOaZVwg==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.10.tgz", + "integrity": "sha512-BxouZUm0I45K4yjOOIzj24nTi0H2cGo0y7xUmk+Po/PYtJXFBYVDS1BguE7t28efXjKdcN0tmiLivxQy//SsZg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.2.2", + "@next/eslint-plugin-next": "16.0.10", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -13580,9 +13431,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -13604,9 +13452,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -13628,9 +13473,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -13652,9 +13494,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -15630,14 +15469,13 @@ } }, "node_modules/next": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", - "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", + "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "license": "MIT", "dependencies": { - "@next/env": "16.2.2", + "@next/env": "16.0.10", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -15649,15 +15487,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.2", - "@next/swc-darwin-x64": "16.2.2", - "@next/swc-linux-arm64-gnu": "16.2.2", - "@next/swc-linux-arm64-musl": "16.2.2", - "@next/swc-linux-x64-gnu": "16.2.2", - "@next/swc-linux-x64-musl": "16.2.2", - "@next/swc-win32-arm64-msvc": "16.2.2", - "@next/swc-win32-x64-msvc": "16.2.2", - "sharp": "^0.34.5" + "@next/swc-darwin-arm64": "16.0.10", + "@next/swc-darwin-x64": "16.0.10", + "@next/swc-linux-arm64-gnu": "16.0.10", + "@next/swc-linux-arm64-musl": "16.0.10", + "@next/swc-linux-x64-gnu": "16.0.10", + "@next/swc-linux-x64-musl": "16.0.10", + "@next/swc-win32-arm64-msvc": "16.0.10", + "@next/swc-win32-x64-msvc": "16.0.10", + "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -21047,7 +20885,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.5.1" + "version": "3.5.2" } } } diff --git a/package.json b/package.json index e97f14f27a..16852c8502 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.5.1", + "version": "3.5.2", "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.", "type": "module", "bin": { @@ -100,7 +100,7 @@ "js-yaml": "^4.1.0", "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", - "next": "^16.0.10", + "next": "16.0.10", "next-intl": "^4.8.3", "node-machine-id": "^1.1.12", "open": "^11.0.0", @@ -138,7 +138,7 @@ "concurrently": "^9.2.1", "cross-env": "^10.1.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.0.10", + "eslint-config-next": "16.0.10", "husky": "^9.1.7", "jsdom": "^29.0.1", "lint-staged": "^16.2.7", diff --git a/scripts/prepublish.mjs b/scripts/prepublish.mjs index 49e0b9e97f..60c1a454d3 100644 --- a/scripts/prepublish.mjs +++ b/scripts/prepublish.mjs @@ -300,7 +300,7 @@ if (existsSync(swcHelpersSrc) && !existsSync(swcHelpersDst)) { // ── Step 10.6: Remove large binaries from standalone build ── // These directories contain platform-native binaries (.node, .asar) that // trigger Z_DATA_ERROR during npm pack. They are not needed in the npm package. -const binaryDirsToRemove = ["vscode-extension", "electron"]; +const binaryDirsToRemove = ["vscode-extension", "electron", "logs"]; for (const dir of binaryDirsToRemove) { const targetDir = join(APP_DIR, dir); if (existsSync(targetDir)) { diff --git a/src/app/globals.css b/src/app/globals.css index ac78afa1ab..a157baac9d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,3 @@ -@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"); @import "tailwindcss"; /* Tailwind v4 auto-detection cannot scan directories with parentheses diff --git a/src/lib/modelsDevSync.ts b/src/lib/modelsDevSync.ts index 320ce047b5..858baced6b 100644 --- a/src/lib/modelsDevSync.ts +++ b/src/lib/modelsDevSync.ts @@ -692,3 +692,12 @@ export async function initModelsDevSync(): Promise { const interval = settings.modelsDevSyncInterval as number | undefined; startPeriodicSync(interval); } + +/** + * Get context window limit for a specific model from synced capabilities. + * Returns null if not available. + */ +export function getModelContextLimit(provider: string, modelId: string): number | null { + const caps = getSyncedCapabilities(provider, modelId); + return caps[provider]?.[modelId]?.limit_context ?? null; +} diff --git a/src/shared/constants/routingStrategies.ts b/src/shared/constants/routingStrategies.ts index 3f91b02bd9..b67908a074 100644 --- a/src/shared/constants/routingStrategies.ts +++ b/src/shared/constants/routingStrategies.ts @@ -9,6 +9,7 @@ export type RoutingStrategyValue = | "cost-optimized" | "strict-random" | "auto" + | "context-optimized" | "lkgp"; type RoutingStrategyOption = { @@ -97,6 +98,13 @@ export const ROUTING_STRATEGIES: RoutingStrategyOption[] = [ settingsDescKey: "lkgpDesc", icon: "verified", }, + { + value: "context-optimized", + labelKey: "contextOpt", + combosDescKey: "contextOptimizedDesc", + settingsDescKey: "contextOptDesc", + icon: "text_snippet", + }, ]; export const SETTINGS_FALLBACK_STRATEGY_VALUES: RoutingStrategyValue[] = [ @@ -110,5 +118,6 @@ export const SETTINGS_FALLBACK_STRATEGY_VALUES: RoutingStrategyValue[] = [ "cost-optimized", "strict-random", "auto", + "context-optimized", "lkgp", ]; diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 68452a75b5..1c0d4a142f 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -95,6 +95,7 @@ const comboStrategySchema = z.enum([ "p2c", "auto", "lkgp", + "context-optimized", ]); const scoringWeightsSchema = z diff --git a/test-antigrav.mjs b/test-antigrav.mjs deleted file mode 100644 index 3578ee5bb6..0000000000 --- a/test-antigrav.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; - -const usageService = await import("./open-sse/services/usage.ts"); - -test("antigravity fraction logic", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = async (url) => { - if (url.includes("loadCodeAssist")) { - return new Response( - JSON.stringify({ - currentTier: { id: "pro", name: "Pro" }, - }), - { status: 200 } - ); - } - if (url.includes("fetchAvailableModels")) { - return new Response( - JSON.stringify({ - models: { - "gemini-1.5-pro": { - quotaInfo: { - remainingFraction: 0, - resetTime: "2025-05-01T00:00:00Z", - }, - }, - }, - }), - { status: 200 } - ); - } - }; - - try { - const res = await usageService.getUsageForProvider({ - provider: "antigravity", - accessToken: "test", - }); - console.log(JSON.stringify(res, null, 2)); - } finally { - globalThis.fetch = originalFetch; - } -}); diff --git a/test-executor.ts b/test-executor.ts new file mode 100644 index 0000000000..754b6f5414 --- /dev/null +++ b/test-executor.ts @@ -0,0 +1,38 @@ +import { QoderExecutor } from "./open-sse/executors/qoder.ts"; +import fs from "fs"; +import os from "os"; + +async function run() { + const credsPath = os.homedir() + "/.qwen/oauth_creds.json"; + const creds = JSON.parse(fs.readFileSync(credsPath, "utf8")); + + const executor = new QoderExecutor(); + const result = await executor.execute({ + model: "coder-model", + body: { + messages: [{ role: "user", content: "hello test" }], + stream: true, + max_tokens: 10, + }, + credentials: { + accessToken: creds.access_token, + resourceUrl: creds.resource_url, + }, + signal: new AbortController().signal, + }); + + console.log("Status:", result.response.status); + + if (result.response.body) { + // If it's a web stream, readable stream + const reader = result.response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + console.log(decoder.decode(value)); + } + } +} + +run().catch(console.error); diff --git a/test-mcp-bundle.js b/test-mcp-bundle.js deleted file mode 100644 index 35608c6875..0000000000 --- a/test-mcp-bundle.js +++ /dev/null @@ -1,3923 +0,0 @@ -// open-sse/mcp-server/server.ts -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; - -// open-sse/mcp-server/schemas/tools.ts -import { z } from "zod"; -var getHealthInput = z.object({}).describe("No parameters required"); -var getHealthOutput = z.object({ - uptime: z.string(), - version: z.string(), - memoryUsage: z.object({ - heapUsed: z.number(), - heapTotal: z.number(), - }), - circuitBreakers: z.array( - z.object({ - provider: z.string(), - state: z.enum(["CLOSED", "OPEN", "HALF_OPEN"]), - failureCount: z.number(), - lastFailure: z.string().nullable(), - }) - ), - rateLimits: z.array( - z.object({ - provider: z.string(), - rpm: z.number(), - currentUsage: z.number(), - isLimited: z.boolean(), - }) - ), - cacheStats: z - .object({ - hits: z.number(), - misses: z.number(), - hitRate: z.number(), - }) - .optional(), -}); -var getHealthTool = { - name: "omniroute_get_health", - description: - "Returns the current health status of OmniRoute including uptime, memory usage, circuit breaker states for all providers, rate limit status, and cache statistics.", - inputSchema: getHealthInput, - outputSchema: getHealthOutput, - scopes: ["read:health"], - auditLevel: "basic", - phase: 1, - sourceEndpoints: ["/api/monitoring/health", "/api/resilience", "/api/rate-limits"], -}; -var listCombosInput = z.object({ - includeMetrics: z - .boolean() - .optional() - .describe("Include request count, success rate, latency, and cost metrics per combo"), -}); -var listCombosOutput = z.object({ - combos: z.array( - z.object({ - id: z.string(), - name: z.string(), - models: z.array( - z.object({ - provider: z.string(), - model: z.string(), - priority: z.number(), - }) - ), - strategy: z.enum([ - "priority", - "weighted", - "round-robin", - "strict-random", - "random", - "least-used", - "cost-optimized", - "auto", - ]), - enabled: z.boolean(), - metrics: z - .object({ - requestCount: z.number(), - successRate: z.number(), - avgLatencyMs: z.number(), - totalCost: z.number(), - }) - .optional(), - }) - ), -}); -var listCombosTool = { - name: "omniroute_list_combos", - description: - "Lists all configured combos (model chains) with their strategies and optionally includes performance metrics. Combos define how requests are routed across multiple providers.", - inputSchema: listCombosInput, - outputSchema: listCombosOutput, - scopes: ["read:combos"], - auditLevel: "basic", - phase: 1, - sourceEndpoints: ["/api/combos", "/api/combos/metrics"], -}; -var getComboMetricsInput = z.object({ - comboId: z.string().describe("ID of the combo to get metrics for"), -}); -var getComboMetricsOutput = z.object({ - requests: z.number(), - successRate: z.number(), - avgLatency: z.number(), - costTotal: z.number(), - fallbackCount: z.number(), - byProvider: z.array( - z.object({ - provider: z.string(), - requests: z.number(), - successRate: z.number(), - avgLatency: z.number(), - }) - ), -}); -var getComboMetricsTool = { - name: "omniroute_get_combo_metrics", - description: - "Returns detailed performance metrics for a specific combo including request count, success rate, average latency, total cost, and per-provider breakdowns.", - inputSchema: getComboMetricsInput, - outputSchema: getComboMetricsOutput, - scopes: ["read:combos"], - auditLevel: "basic", - phase: 1, - sourceEndpoints: ["/api/combos/metrics"], -}; -var switchComboInput = z.object({ - comboId: z.string().describe("ID of the combo to activate/deactivate"), - active: z.boolean().describe("Whether to enable or disable the combo"), -}); -var switchComboOutput = z.object({ - success: z.boolean(), - combo: z.object({ - id: z.string(), - name: z.string(), - enabled: z.boolean(), - }), -}); -var switchComboTool = { - name: "omniroute_switch_combo", - description: - "Activates or deactivates a combo. When deactivated, requests will not be routed through this combo. Use to toggle between different routing strategies.", - inputSchema: switchComboInput, - outputSchema: switchComboOutput, - scopes: ["write:combos"], - auditLevel: "full", - phase: 1, - sourceEndpoints: ["/api/combos"], -}; -var checkQuotaInput = z.object({ - provider: z - .string() - .optional() - .describe( - "Filter by provider name (e.g., 'claude', 'gemini'). If omitted, returns all providers." - ), - connectionId: z.string().optional().describe("Filter by specific connection ID"), -}); -var checkQuotaOutput = z.object({ - providers: z.array( - z.object({ - name: z.string(), - provider: z.string(), - connectionId: z.string(), - quotaUsed: z.number(), - quotaTotal: z.number().nullable(), - percentRemaining: z.number(), - resetAt: z.string().nullable(), - tokenStatus: z.enum(["valid", "expiring", "expired", "refreshing"]), - }) - ), - meta: z - .object({ - generatedAt: z.string(), - filters: z.object({ - provider: z.string().nullable(), - connectionId: z.string().nullable(), - }), - totalProviders: z.number(), - }) - .optional(), -}); -var checkQuotaTool = { - name: "omniroute_check_quota", - description: - "Checks the remaining API quota for one or all providers. Returns quota used/total, percentage remaining, reset time, and token health status.", - inputSchema: checkQuotaInput, - outputSchema: checkQuotaOutput, - scopes: ["read:quota"], - auditLevel: "basic", - phase: 1, - sourceEndpoints: ["/api/usage/quota", "/api/token-health", "/api/rate-limits"], -}; -var routeRequestInput = z.object({ - model: z.string().describe("Model identifier (e.g., 'claude-sonnet-4', 'gpt-4o')"), - messages: z - .array( - z.object({ - role: z.string(), - content: z.string(), - }) - ) - .describe("Chat messages in OpenAI format"), - combo: z.string().optional().describe("Specific combo to route through"), - budget: z.number().optional().describe("Maximum cost in USD for this request"), - role: z - .enum(["coding", "review", "planning", "analysis"]) - .optional() - .describe("Task role hint for intelligent routing"), - stream: z.boolean().optional().default(false).describe("Whether to stream the response"), -}); -var routeRequestOutput = z.object({ - response: z.object({ - content: z.string(), - model: z.string(), - tokens: z.object({ - prompt: z.number(), - completion: z.number(), - }), - }), - routing: z.object({ - provider: z.string(), - combo: z.string().nullable(), - fallbacksTriggered: z.number(), - cost: z.number(), - latencyMs: z.number(), - routingExplanation: z.string(), - }), -}); -var routeRequestTool = { - name: "omniroute_route_request", - description: - "Sends a chat completion request through OmniRoute's intelligent routing pipeline. Supports combo selection, budget limits, and task role hints for optimal provider matching.", - inputSchema: routeRequestInput, - outputSchema: routeRequestOutput, - scopes: ["execute:completions"], - auditLevel: "full", - phase: 1, - sourceEndpoints: ["/v1/chat/completions", "/v1/responses"], -}; -var costReportInput = z.object({ - period: z - .enum(["session", "day", "week", "month"]) - .optional() - .default("session") - .describe("Time period for the cost report"), -}); -var costReportOutput = z.object({ - period: z.string(), - totalCost: z.number(), - requestCount: z.number(), - tokenCount: z.object({ - prompt: z.number(), - completion: z.number(), - }), - byProvider: z.array( - z.object({ - name: z.string(), - cost: z.number(), - requests: z.number(), - }) - ), - byModel: z.array( - z.object({ - model: z.string(), - cost: z.number(), - requests: z.number(), - }) - ), - budget: z.object({ - limit: z.number().nullable(), - remaining: z.number().nullable(), - }), -}); -var costReportTool = { - name: "omniroute_cost_report", - description: - "Generates a cost report for the specified period showing total cost, request count, token usage, and breakdowns by provider and model. Also shows budget status if configured.", - inputSchema: costReportInput, - outputSchema: costReportOutput, - scopes: ["read:usage"], - auditLevel: "basic", - phase: 1, - sourceEndpoints: ["/api/usage/analytics", "/api/usage/budget"], -}; -var listModelsCatalogInput = z.object({ - provider: z.string().optional().describe("Filter by provider name"), - capability: z - .enum(["chat", "embedding", "image", "audio", "video", "rerank", "moderation"]) - .optional() - .describe("Filter by model capability"), -}); -var listModelsCatalogOutput = z.object({ - models: z.array( - z.object({ - id: z.string(), - provider: z.string(), - capabilities: z.array(z.string()), - status: z.enum(["available", "degraded", "unavailable"]), - pricing: z - .object({ - inputPerMillion: z.number().nullable(), - outputPerMillion: z.number().nullable(), - }) - .optional(), - }) - ), -}); -var listModelsCatalogTool = { - name: "omniroute_list_models_catalog", - description: - "Lists all available AI models across all providers with their capabilities, current status, and pricing information.", - inputSchema: listModelsCatalogInput, - outputSchema: listModelsCatalogOutput, - scopes: ["read:models"], - auditLevel: "none", - phase: 1, - sourceEndpoints: ["/api/models/catalog", "/v1/models"], -}; -var webSearchInput = z.object({ - query: z - .string() - .min(1, "Query is required") - .max(1e3, "Query must be 1000 characters or fewer") - .describe("The search query string"), - max_results: z - .number() - .int() - .min(1) - .max(20) - .default(5) - .describe("Maximum number of search results to return"), - search_type: z.enum(["web", "news"]).default("web").describe("Type of search to perform"), - provider: z - .string() - .optional() - .describe("Specific search provider to use (serper, brave, perplexity, exa, tavily)"), -}); -var webSearchOutput = z.object({ - id: z.string(), - provider: z.string(), - query: z.string(), - results: z.array( - z.object({ - title: z.string(), - url: z.string(), - display_url: z.string().optional(), - snippet: z.string(), - position: z.number().int().positive(), - }) - ), - cached: z.boolean(), - usage: z.object({ - queries_used: z.number().int().min(0), - search_cost_usd: z.number().min(0), - }), -}); -var webSearchTool = { - name: "omniroute_web_search", - description: - "Performs a web search using OmniRoute's search gateway. Supports multiple providers (Serper, Brave, Perplexity, Exa, Tavily) with automatic failover. Returns search results with titles, URLs, snippets, and position data.", - inputSchema: webSearchInput, - outputSchema: webSearchOutput, - scopes: ["execute:search"], - auditLevel: "basic", - phase: 1, - sourceEndpoints: ["/v1/search"], -}; -var simulateRouteInput = z.object({ - model: z.string().describe("Target model for simulation"), - promptTokenEstimate: z.number().describe("Estimated prompt token count"), - combo: z.string().optional().describe("Specific combo to simulate (default: active combo)"), -}); -var simulateRouteOutput = z.object({ - simulatedPath: z.array( - z.object({ - provider: z.string(), - model: z.string(), - probability: z.number(), - estimatedCost: z.number(), - healthStatus: z.enum(["CLOSED", "OPEN", "HALF_OPEN"]), - quotaAvailable: z.number(), - }) - ), - fallbackTree: z.object({ - primary: z.string(), - fallbacks: z.array(z.string()), - worstCaseCost: z.number(), - bestCaseCost: z.number(), - }), -}); -var simulateRouteTool = { - name: "omniroute_simulate_route", - description: - "Simulates (dry-run) the routing path a request would take without actually executing it. Shows the fallback tree, provider probabilities, estimated costs, and health status.", - inputSchema: simulateRouteInput, - outputSchema: simulateRouteOutput, - scopes: ["read:health", "read:combos"], - auditLevel: "basic", - phase: 2, - sourceEndpoints: ["/api/combos", "/api/monitoring/health", "/api/resilience"], -}; -var setBudgetGuardInput = z.object({ - maxCost: z.number().describe("Maximum cost in USD for this session"), - action: z.enum(["degrade", "block", "alert"]).describe("Action when budget is exceeded"), - degradeToTier: z - .enum(["cheap", "free"]) - .optional() - .describe("If action=degrade, which tier to fall back to"), -}); -var setBudgetGuardOutput = z.object({ - sessionId: z.string(), - budgetTotal: z.number(), - budgetSpent: z.number(), - budgetRemaining: z.number(), - action: z.string(), - status: z.enum(["active", "warning", "exceeded"]), -}); -var setBudgetGuardTool = { - name: "omniroute_set_budget_guard", - description: - "Sets a budget guard that limits spending for the current session. When the budget is reached, it can degrade to cheaper models, block requests, or send alerts.", - inputSchema: setBudgetGuardInput, - outputSchema: setBudgetGuardOutput, - scopes: ["write:budget"], - auditLevel: "full", - phase: 2, - sourceEndpoints: ["/api/usage/budget"], -}; -var setRoutingStrategyInput = z.object({ - comboId: z.string().describe("Combo ID or name to update"), - strategy: z - .enum([ - "priority", - "weighted", - "round-robin", - "strict-random", - "random", - "least-used", - "cost-optimized", - "auto", - ]) - .describe("Routing strategy to apply"), - autoRoutingStrategy: z - .enum(["rules", "cost", "eco", "latency", "fast"]) - .optional() - .describe("Optional strategy used by auto mode (only used when strategy='auto')"), -}); -var setRoutingStrategyOutput = z.object({ - success: z.boolean(), - combo: z.object({ - id: z.string(), - name: z.string(), - strategy: z.string(), - autoRoutingStrategy: z.string().nullable(), - }), -}); -var setRoutingStrategyTool = { - name: "omniroute_set_routing_strategy", - description: - "Updates a combo routing strategy (priority/weighted/auto/etc.) at runtime. Supports selecting the sub-strategy used by auto mode (rules/cost/latency).", - inputSchema: setRoutingStrategyInput, - outputSchema: setRoutingStrategyOutput, - scopes: ["write:combos"], - auditLevel: "full", - phase: 2, - sourceEndpoints: ["/api/combos", "/api/combos/{id}"], -}; -var setResilienceProfileInput = z.object({ - profile: z - .enum(["aggressive", "balanced", "conservative"]) - .describe("Resilience profile to apply"), -}); -var setResilienceProfileOutput = z.object({ - applied: z.boolean(), - settings: z.object({ - circuitBreakerThreshold: z.number(), - retryCount: z.number(), - timeoutMs: z.number(), - fallbackDepth: z.number(), - }), -}); -var setResilienceProfileTool = { - name: "omniroute_set_resilience_profile", - description: - "Applies a resilience profile that adjusts circuit breaker thresholds, retry counts, timeouts, and fallback depth. 'aggressive' = fast fail, 'conservative' = max retries.", - inputSchema: setResilienceProfileInput, - outputSchema: setResilienceProfileOutput, - scopes: ["write:resilience"], - auditLevel: "full", - phase: 2, - sourceEndpoints: ["/api/resilience"], -}; -var testComboInput = z.object({ - comboId: z.string().describe("ID of the combo to test"), - testPrompt: z.string().max(500).describe("Short test prompt (max 500 chars)"), -}); -var testComboOutput = z.object({ - results: z.array( - z.object({ - provider: z.string(), - model: z.string(), - success: z.boolean(), - latencyMs: z.number(), - cost: z.number(), - tokenCount: z.number(), - error: z.string().optional(), - }) - ), - summary: z.object({ - totalProviders: z.number(), - successful: z.number(), - fastestProvider: z.string(), - cheapestProvider: z.string(), - }), -}); -var testComboTool = { - name: "omniroute_test_combo", - description: - "Tests a combo by sending a short test prompt to each provider in the combo and reporting individual results including latency, cost, and success status.", - inputSchema: testComboInput, - outputSchema: testComboOutput, - scopes: ["execute:completions", "read:combos"], - auditLevel: "full", - phase: 2, - sourceEndpoints: ["/api/combos/test", "/v1/chat/completions"], -}; -var getProviderMetricsInput = z.object({ - provider: z.string().describe("Provider name (e.g., 'claude', 'gemini-cli', 'codex')"), -}); -var getProviderMetricsOutput = z.object({ - provider: z.string(), - successRate: z.number(), - requestCount: z.number(), - avgLatencyMs: z.number(), - p50LatencyMs: z.number(), - p95LatencyMs: z.number(), - p99LatencyMs: z.number(), - errorRate: z.number(), - lastError: z - .object({ - message: z.string(), - timestamp: z.string(), - }) - .nullable(), - circuitBreakerState: z.enum(["CLOSED", "OPEN", "HALF_OPEN"]), - quotaInfo: z.object({ - used: z.number(), - total: z.number().nullable(), - resetAt: z.string().nullable(), - }), -}); -var getProviderMetricsTool = { - name: "omniroute_get_provider_metrics", - description: - "Returns detailed performance metrics for a specific provider including success/error rates, latency percentiles (p50/p95/p99), circuit breaker state, and quota information.", - inputSchema: getProviderMetricsInput, - outputSchema: getProviderMetricsOutput, - scopes: ["read:health"], - auditLevel: "basic", - phase: 2, - sourceEndpoints: ["/api/provider-metrics", "/api/resilience"], -}; -var bestComboForTaskInput = z.object({ - taskType: z - .enum(["coding", "review", "planning", "analysis", "debugging", "documentation"]) - .describe("Type of task to find the best combo for"), - budgetConstraint: z.number().optional().describe("Maximum cost in USD"), - latencyConstraint: z.number().optional().describe("Maximum acceptable latency in ms"), -}); -var bestComboForTaskOutput = z.object({ - recommendedCombo: z.object({ - id: z.string(), - name: z.string(), - reason: z.string(), - }), - alternatives: z.array( - z.object({ - id: z.string(), - name: z.string(), - tradeoff: z.string(), - }) - ), - freeAlternative: z - .object({ - id: z.string(), - name: z.string(), - }) - .nullable(), -}); -var bestComboForTaskTool = { - name: "omniroute_best_combo_for_task", - description: - "Recommends the best combo for a given task type (coding, review, planning, etc.) considering budget and latency constraints. Also suggests alternatives and free options.", - inputSchema: bestComboForTaskInput, - outputSchema: bestComboForTaskOutput, - scopes: ["read:combos", "read:health"], - auditLevel: "basic", - phase: 2, - sourceEndpoints: ["/api/combos", "/api/combos/metrics", "/api/monitoring/health"], -}; -var explainRouteInput = z.object({ - requestId: z.string().describe("Request ID from the X-Request-Id header"), -}); -var explainRouteOutput = z.object({ - requestId: z.string(), - decision: z.object({ - comboUsed: z.string(), - providerSelected: z.string(), - modelUsed: z.string(), - score: z.number(), - factors: z.array( - z.object({ - name: z.string(), - value: z.number(), - weight: z.number(), - contribution: z.number(), - }) - ), - fallbacksTriggered: z.array( - z.object({ - provider: z.string(), - reason: z.string(), - }) - ), - costActual: z.number(), - latencyActual: z.number(), - }), -}); -var explainRouteTool = { - name: "omniroute_explain_route", - description: - "Explains why a specific request was routed to a particular provider. Shows the scoring factors, weights, fallbacks triggered, actual cost, and latency.", - inputSchema: explainRouteInput, - outputSchema: explainRouteOutput, - scopes: ["read:health", "read:usage"], - auditLevel: "basic", - phase: 2, - sourceEndpoints: [], -}; -var getSessionSnapshotInput = z.object({}).describe("No parameters required"); -var getSessionSnapshotOutput = z.object({ - sessionStart: z.string(), - duration: z.string(), - requestCount: z.number(), - costTotal: z.number(), - tokenCount: z.object({ - prompt: z.number(), - completion: z.number(), - }), - topModels: z.array( - z.object({ - model: z.string(), - count: z.number(), - }) - ), - topProviders: z.array( - z.object({ - provider: z.string(), - count: z.number(), - }) - ), - errors: z.number(), - fallbacks: z.number(), - budgetGuard: z - .object({ - active: z.boolean(), - remaining: z.number(), - }) - .nullable(), -}); -var getSessionSnapshotTool = { - name: "omniroute_get_session_snapshot", - description: - "Returns a snapshot of the current working session including duration, request count, total cost, top models/providers used, error count, and budget guard status.", - inputSchema: getSessionSnapshotInput, - outputSchema: getSessionSnapshotOutput, - scopes: ["read:usage"], - auditLevel: "none", - phase: 2, - sourceEndpoints: ["/api/usage/analytics", "/api/telemetry/summary"], -}; -var syncPricingInput = z.object({ - sources: z - .array(z.string()) - .optional() - .describe("External pricing sources to sync from (default: ['litellm'])"), - dryRun: z - .boolean() - .optional() - .describe("If true, preview sync results without saving to database"), -}); -var syncPricingOutput = z.object({ - success: z.boolean(), - modelCount: z.number(), - providerCount: z.number(), - source: z.string(), - dryRun: z.boolean(), - error: z.string().optional(), - warnings: z.array(z.string()).optional(), - data: z.record(z.string(), z.record(z.string(), z.unknown())).optional(), -}); -var syncPricingTool = { - name: "omniroute_sync_pricing", - description: - "Syncs pricing data from external sources (LiteLLM) into OmniRoute. Synced pricing fills gaps not covered by hardcoded defaults without overwriting user-set prices. Use dryRun=true to preview.", - inputSchema: syncPricingInput, - outputSchema: syncPricingOutput, - scopes: ["pricing:write"], - auditLevel: "full", - phase: 2, - sourceEndpoints: ["/api/pricing/sync"], -}; -var cacheStatsInput = z.object({}).describe("No parameters required"); -var cacheStatsOutput = z.object({ - semanticCache: z.object({ - memoryEntries: z.number(), - dbEntries: z.number(), - hits: z.number(), - misses: z.number(), - hitRate: z.string(), - tokensSaved: z.number(), - }), - promptCache: z - .object({ - totalRequests: z.number(), - requestsWithCacheControl: z.number(), - totalCachedTokens: z.number(), - totalCacheCreationTokens: z.number(), - estimatedCostSaved: z.number(), - }) - .nullable(), - idempotency: z.object({ - activeKeys: z.number(), - windowMs: z.number(), - }), -}); -var cacheStatsTool = { - name: "omniroute_cache_stats", - description: - "Returns cache statistics including semantic cache hit rate, prompt cache metrics by provider, and idempotency layer stats.", - inputSchema: cacheStatsInput, - outputSchema: cacheStatsOutput, - scopes: ["read:cache"], - auditLevel: "basic", - phase: 2, - sourceEndpoints: ["/api/cache"], -}; -var cacheFlushInput = z.object({ - signature: z.string().optional().describe("Specific cache signature to invalidate"), - model: z.string().optional().describe("Invalidate all entries for a specific model"), -}); -var cacheFlushOutput = z.object({ - ok: z.boolean(), - invalidated: z.number().optional(), - scope: z.string().optional(), -}); -var cacheFlushTool = { - name: "omniroute_cache_flush", - description: - "Flush cache entries. Provide signature to invalidate a single entry, model to invalidate all entries for a model, or omit both to clear all.", - inputSchema: cacheFlushInput, - outputSchema: cacheFlushOutput, - scopes: ["write:cache"], - auditLevel: "full", - phase: 2, - sourceEndpoints: ["/api/cache"], -}; -var MCP_TOOLS = [ - getHealthTool, - listCombosTool, - getComboMetricsTool, - switchComboTool, - checkQuotaTool, - routeRequestTool, - costReportTool, - listModelsCatalogTool, - webSearchTool, - simulateRouteTool, - setBudgetGuardTool, - setRoutingStrategyTool, - setResilienceProfileTool, - testComboTool, - getProviderMetricsTool, - bestComboForTaskTool, - explainRouteTool, - getSessionSnapshotTool, - syncPricingTool, - cacheStatsTool, - cacheFlushTool, -]; -var MCP_ESSENTIAL_TOOLS = MCP_TOOLS.filter((t) => t.phase === 1); -var MCP_ADVANCED_TOOLS = MCP_TOOLS.filter((t) => t.phase === 2); -var MCP_TOOL_MAP = Object.fromEntries(MCP_TOOLS.map((t) => [t.name, t])); - -// open-sse/mcp-server/runtimeHeartbeat.ts -import { promises as fs } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; -var HEARTBEAT_FILE = "mcp-heartbeat.json"; -var RUNTIME_DIR = "runtime"; -var DEFAULT_INTERVAL_MS = 5e3; -function resolveDataDir() { - const configured = process.env.DATA_DIR; - if (typeof configured === "string" && configured.trim().length > 0) { - return configured.trim(); - } - return join(homedir(), ".omniroute"); -} -function resolveMcpHeartbeatPath() { - return join(resolveDataDir(), RUNTIME_DIR, HEARTBEAT_FILE); -} -async function writeHeartbeat(snapshot) { - const heartbeatPath = resolveMcpHeartbeatPath(); - const runtimeDir = join(resolveDataDir(), RUNTIME_DIR); - await fs.mkdir(runtimeDir, { recursive: true }); - await fs.writeFile(heartbeatPath, JSON.stringify(snapshot, null, 2), "utf-8"); -} -function startMcpHeartbeat(config) { - const startedAt = /* @__PURE__ */ new Date().toISOString(); - let timer = null; - let stopped = false; - const intervalMs = - typeof config.intervalMs === "number" && config.intervalMs > 0 - ? config.intervalMs - : DEFAULT_INTERVAL_MS; - const tick = async () => { - if (stopped) return; - const snapshot = { - pid: process.pid, - startedAt, - lastHeartbeatAt: /* @__PURE__ */ new Date().toISOString(), - version: config.version, - transport: "stdio", - scopesEnforced: config.scopesEnforced, - allowedScopes: [...config.allowedScopes], - toolCount: config.toolCount, - }; - try { - await writeHeartbeat(snapshot); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error("[MCP Heartbeat] Failed to write heartbeat:", message); - } - }; - void tick(); - timer = setInterval(() => { - void tick(); - }, intervalMs); - return () => { - if (stopped) return; - stopped = true; - if (timer) { - clearInterval(timer); - timer = null; - } - void tick(); - }; -} - -// open-sse/mcp-server/schemas/audit.ts -async function hashInput(input) { - const data = JSON.stringify(input); - const encoder = new TextEncoder(); - const hashBuffer = await crypto.subtle.digest("SHA-256", encoder.encode(data)); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); -} -function summarizeOutput(output, maxLength = 200) { - if (output === null || output === void 0) return "(null)"; - const str = typeof output === "string" ? output : JSON.stringify(output); - if (str.length <= maxLength) return str; - return str.slice(0, maxLength) + "\u2026"; -} - -// open-sse/mcp-server/audit.ts -var db = null; -async function getDb() { - if (db) return db; - try { - const { homedir: homedir2 } = await import("node:os"); - const { join: join2 } = await import("node:path"); - const { existsSync } = await import("node:fs"); - const dbPath = process.env.DATA_DIR - ? join2(process.env.DATA_DIR, "storage.sqlite") - : join2(homedir2(), ".omniroute", "storage.sqlite"); - if (!existsSync(dbPath)) { - console.error(`[MCP Audit] Database not found at ${dbPath} \u2014 audit logging disabled`); - return null; - } - const Database2 = (await import("better-sqlite3")).default; - db = new Database2(dbPath); - return db; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error("[MCP Audit] Failed to connect to database:", message); - return null; - } -} -async function logToolCall(toolName, input, output, durationMs, success, errorCode) { - try { - const database = await getDb(); - if (!database) return; - const inputHash = await hashInput(input); - const outputSummary = summarizeOutput(output); - const apiKeyId = process.env.OMNIROUTE_API_KEY_ID || null; - database - .prepare( - `INSERT INTO mcp_tool_audit (tool_name, input_hash, output_summary, duration_ms, api_key_id, success, error_code) - VALUES (?, ?, ?, ?, ?, ?, ?)` - ) - .run( - toolName, - inputHash, - outputSummary, - durationMs, - apiKeyId, - success ? 1 : 0, - errorCode || null - ); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error("[MCP Audit] Failed to log:", message); - } -} - -// open-sse/mcp-server/scopeEnforcement.ts -function normalizeScopeList(raw) { - if (!Array.isArray(raw)) return []; - const normalized = raw - .filter((value) => typeof value === "string") - .map((value) => value.trim()) - .filter(Boolean); - return Array.from(new Set(normalized)); -} -function extractMetaScopeList(meta) { - if (!meta || typeof meta !== "object") return []; - const metaRecord = meta; - const direct = normalizeScopeList(metaRecord.scopes); - if (direct.length > 0) return direct; - const auth = metaRecord.auth; - if (auth && typeof auth === "object") { - const authScopes = normalizeScopeList(auth.scopes); - if (authScopes.length > 0) return authScopes; - } - const omni = metaRecord.omniroute; - if (omni && typeof omni === "object") { - const omniScopes = normalizeScopeList(omni.scopes); - if (omniScopes.length > 0) return omniScopes; - } - return []; -} -function scopeMatches(grantedScope, requiredScope) { - if (grantedScope === "*" || grantedScope === requiredScope) { - return true; - } - if (grantedScope.endsWith("*")) { - const prefix = grantedScope.slice(0, -1); - return requiredScope.startsWith(prefix); - } - return false; -} -function resolveCallerScopeContext(extra, fallbackScopes = []) { - const callerId = - (typeof extra?.authInfo?.clientId === "string" && extra.authInfo.clientId.trim()) || - (typeof extra?.sessionId === "string" && extra.sessionId.trim()) || - "anonymous"; - const authScopes = normalizeScopeList(extra?.authInfo?.scopes); - if (authScopes.length > 0) { - return { callerId, scopes: authScopes, source: "authInfo" }; - } - const metaScopes = extractMetaScopeList(extra?._meta); - if (metaScopes.length > 0) { - return { callerId, scopes: metaScopes, source: "meta" }; - } - const fallback = normalizeScopeList(fallbackScopes); - if (fallback.length > 0) { - return { callerId, scopes: fallback, source: "env" }; - } - return { callerId, scopes: [], source: "none" }; -} -function evaluateToolScopes(toolName, callerScopes, enforceScopes) { - const toolDef = MCP_TOOL_MAP[toolName]; - if (!toolDef) { - return { - allowed: false, - required: [], - provided: Array.from(callerScopes), - missing: [], - reason: "tool_definition_missing", - }; - } - const required = Array.isArray(toolDef.scopes) ? Array.from(toolDef.scopes) : []; - const provided = normalizeScopeList(callerScopes); - if (!enforceScopes || required.length === 0) { - return { allowed: true, required, provided, missing: [] }; - } - const missing = required.filter( - (requiredScope) => !provided.some((grantedScope) => scopeMatches(grantedScope, requiredScope)) - ); - return { - allowed: missing.length === 0, - required, - provided, - missing, - reason: missing.length > 0 ? "missing_scopes" : void 0, - }; -} - -// src/shared/contracts/quota.ts -var TOKEN_STATUS_VALUES = ["valid", "expiring", "expired", "refreshing"]; -function toNumber(value) { - if (typeof value === "number" && Number.isFinite(value)) return value; - if (typeof value === "string" && value.trim() !== "") { - const parsed = Number(value); - if (Number.isFinite(parsed)) return parsed; - } - return null; -} -function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); -} -function normalizeTokenStatus(value) { - if (typeof value === "string" && TOKEN_STATUS_VALUES.includes(value)) { - return value; - } - return "valid"; -} -function sanitizeQuotaProvider(input) { - const source = input && typeof input === "object" ? input : {}; - const provider = typeof source.provider === "string" ? source.provider : "unknown"; - const name = typeof source.name === "string" && source.name.trim() ? source.name : provider; - const connectionId = - typeof source.connectionId === "string" && source.connectionId.trim() - ? source.connectionId - : "unknown"; - const quotaTotalRaw = toNumber(source.quotaTotal); - const quotaTotal = quotaTotalRaw !== null && quotaTotalRaw >= 0 ? quotaTotalRaw : null; - const quotaUsedRaw = toNumber(source.quotaUsed) ?? 0; - const quotaUsed = - quotaTotal !== null ? clamp(quotaUsedRaw, 0, quotaTotal) : Math.max(0, quotaUsedRaw); - let percentRemainingRaw = toNumber(source.percentRemaining); - if (percentRemainingRaw === null) { - if (quotaTotal && quotaTotal > 0) { - percentRemainingRaw = ((quotaTotal - quotaUsed) / quotaTotal) * 100; - } else { - percentRemainingRaw = 100; - } - } - const percentRemaining = clamp(percentRemainingRaw, 0, 100); - const resetAt = - typeof source.resetAt === "string" && source.resetAt.trim() ? source.resetAt : null; - return { - name, - provider, - connectionId, - quotaUsed, - quotaTotal, - percentRemaining, - resetAt, - tokenStatus: normalizeTokenStatus(source.tokenStatus), - }; -} -function normalizeQuotaResponse(raw, filters = {}) { - const source = raw && typeof raw === "object" ? raw : {}; - const providersRaw = Array.isArray(source.providers) - ? source.providers - : Array.isArray(raw) - ? raw - : []; - const providers = providersRaw.map((entry) => sanitizeQuotaProvider(entry)); - const sourceMeta = source.meta && typeof source.meta === "object" ? source.meta : {}; - const sourceFilters = - sourceMeta.filters && typeof sourceMeta.filters === "object" ? sourceMeta.filters : {}; - const providerFilter = - filters.provider ?? - (typeof sourceFilters.provider === "string" && sourceFilters.provider.trim() - ? sourceFilters.provider - : null); - const connectionFilter = - filters.connectionId ?? - (typeof sourceFilters.connectionId === "string" && sourceFilters.connectionId.trim() - ? sourceFilters.connectionId - : null); - const generatedAt = - typeof sourceMeta.generatedAt === "string" && sourceMeta.generatedAt.trim() - ? sourceMeta.generatedAt - : /* @__PURE__ */ new Date().toISOString(); - return { - providers, - meta: { - generatedAt, - filters: { - provider: providerFilter || null, - connectionId: connectionFilter || null, - }, - totalProviders: providers.length, - }, - }; -} - -// open-sse/mcp-server/tools/advancedTools.ts -var OMNIROUTE_BASE_URL = process.env.OMNIROUTE_BASE_URL || "http://localhost:20128"; -var OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || ""; -async function apiFetch(path4, options = {}) { - const url = `${OMNIROUTE_BASE_URL}${path4}`; - const headers = { - "Content-Type": "application/json", - ...(OMNIROUTE_API_KEY ? { Authorization: `Bearer ${OMNIROUTE_API_KEY}` } : {}), - ...(options.headers || {}), - }; - const response = await fetch(url, { ...options, headers, signal: AbortSignal.timeout(3e4) }); - if (!response.ok) { - const text = await response.text().catch(() => "Unknown error"); - throw new Error(`API [${response.status}]: ${text}`); - } - return response.json(); -} -function isRecord(value) { - return !!value && typeof value === "object" && !Array.isArray(value); -} -function toRecord(value) { - return isRecord(value) ? value : {}; -} -function toArrayOfRecords(value) { - return Array.isArray(value) ? value.filter(isRecord) : []; -} -function toString(value, fallback = "") { - return typeof value === "string" ? value : fallback; -} -function toNumber2(value, fallback = 0) { - const parsed = - typeof value === "number" - ? value - : typeof value === "string" && value.trim().length > 0 - ? Number(value) - : Number.NaN; - return Number.isFinite(parsed) ? parsed : fallback; -} -function getComboModels(combo) { - const directModels = toArrayOfRecords(combo.models); - const nestedModels = toArrayOfRecords(toRecord(combo.data).models); - const sourceModels = directModels.length > 0 ? directModels : nestedModels; - return sourceModels.map((model) => ({ - provider: toString(model.provider, "unknown"), - model: toString(model.model, ""), - inputCostPer1M: toNumber2(model.inputCostPer1M, 3), - })); -} -function normalizeCombosResponse(raw) { - if (Array.isArray(raw)) return raw.filter(isRecord); - const source = toRecord(raw); - return Array.isArray(source.combos) ? source.combos.filter(isRecord) : []; -} -var activeBudgetGuard = null; -var RESILIENCE_PROFILES = { - aggressive: { - profiles: { - oauth: { - transientCooldown: 3e3, - rateLimitCooldown: 3e4, - maxBackoffLevel: 4, - circuitBreakerThreshold: 2, - circuitBreakerReset: 3e4, - }, - apikey: { - transientCooldown: 2e3, - rateLimitCooldown: 0, - maxBackoffLevel: 3, - circuitBreakerThreshold: 3, - circuitBreakerReset: 15e3, - }, - }, - defaults: { - requestsPerMinute: 180, - minTimeBetweenRequests: 100, - concurrentRequests: 16, - }, - }, - balanced: { - profiles: { - oauth: { - transientCooldown: 5e3, - rateLimitCooldown: 6e4, - maxBackoffLevel: 8, - circuitBreakerThreshold: 3, - circuitBreakerReset: 6e4, - }, - apikey: { - transientCooldown: 3e3, - rateLimitCooldown: 0, - maxBackoffLevel: 5, - circuitBreakerThreshold: 5, - circuitBreakerReset: 3e4, - }, - }, - defaults: { - requestsPerMinute: 100, - minTimeBetweenRequests: 200, - concurrentRequests: 10, - }, - }, - conservative: { - profiles: { - oauth: { - transientCooldown: 8e3, - rateLimitCooldown: 12e4, - maxBackoffLevel: 10, - circuitBreakerThreshold: 8, - circuitBreakerReset: 12e4, - }, - apikey: { - transientCooldown: 5e3, - rateLimitCooldown: 3e4, - maxBackoffLevel: 8, - circuitBreakerThreshold: 8, - circuitBreakerReset: 6e4, - }, - }, - defaults: { - requestsPerMinute: 60, - minTimeBetweenRequests: 350, - concurrentRequests: 6, - }, - }, -}; -var TASK_FITNESS = { - coding: { preferred: ["claude", "deepseek", "codex"], traits: ["fast", "code-optimized"] }, - review: { preferred: ["claude", "gemini", "openai"], traits: ["analytical", "thorough"] }, - planning: { preferred: ["gemini", "claude", "openai"], traits: ["reasoning", "structured"] }, - analysis: { preferred: ["gemini", "claude"], traits: ["deep-reasoning", "large-context"] }, - debugging: { preferred: ["claude", "deepseek", "codex"], traits: ["code-aware", "fast"] }, - documentation: { preferred: ["gemini", "claude", "openai"], traits: ["clear", "structured"] }, -}; -async function handleSimulateRoute(args) { - const start = Date.now(); - try { - const [combosRaw, healthRaw, quotaRaw] = await Promise.allSettled([ - apiFetch("/api/combos"), - apiFetch("/api/monitoring/health"), - apiFetch("/api/usage/quota"), - ]); - const combos = combosRaw.status === "fulfilled" ? normalizeCombosResponse(combosRaw.value) : []; - const health = healthRaw.status === "fulfilled" ? toRecord(healthRaw.value) : {}; - const quota = - quotaRaw.status === "fulfilled" - ? normalizeQuotaResponse(quotaRaw.value) - : normalizeQuotaResponse({}); - const targetCombo = args.combo - ? combos.find( - (combo) => toString(combo.id) === args.combo || toString(combo.name) === args.combo - ) - : combos.find((combo) => combo.enabled !== false); - if (!targetCombo) { - return { - content: [{ type: "text", text: JSON.stringify({ error: "No matching combo found" }) }], - isError: true, - }; - } - const models = getComboModels(targetCombo); - const breakers = toArrayOfRecords(health.circuitBreakers); - const providers = quota.providers; - const simulatedPath = models.map((model, idx) => { - const cb = breakers.find((breaker) => toString(breaker.provider) === model.provider); - const q = providers.find((providerEntry) => providerEntry.provider === model.provider); - const estimatedCost = (args.promptTokenEstimate / 1e6) * model.inputCostPer1M; - return { - provider: model.provider, - model: model.model || args.model, - probability: idx === 0 ? 0.85 : 0.15 / Math.max(models.length - 1, 1), - estimatedCost: Math.round(estimatedCost * 1e4) / 1e4, - healthStatus: toString(cb?.state, "CLOSED"), - quotaAvailable: q?.percentRemaining ?? 100, - }; - }); - const costs = simulatedPath.map((pathEntry) => pathEntry.estimatedCost); - const result = { - simulatedPath, - fallbackTree: { - primary: simulatedPath[0]?.provider || "unknown", - fallbacks: simulatedPath.slice(1).map((pathEntry) => pathEntry.provider), - worstCaseCost: Math.max(...costs, 0), - bestCaseCost: Math.min(...costs, 0), - }, - }; - await logToolCall("omniroute_simulate_route", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_simulate_route", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleSetBudgetGuard(args) { - const start = Date.now(); - try { - let spent = 0; - try { - const analytics = toRecord(await apiFetch("/api/usage/analytics?period=session")); - spent = toNumber2(analytics.totalCost, 0); - } catch {} - activeBudgetGuard = { - sessionId: `budget_${Date.now()}`, - maxCost: args.maxCost, - action: args.action, - degradeToTier: args.degradeToTier, - spent, - createdAt: /* @__PURE__ */ new Date().toISOString(), - }; - const remaining = Math.max(0, args.maxCost - spent); - const result = { - sessionId: activeBudgetGuard.sessionId, - budgetTotal: args.maxCost, - budgetSpent: Math.round(spent * 1e4) / 1e4, - budgetRemaining: Math.round(remaining * 1e4) / 1e4, - action: args.action, - status: remaining <= 0 ? "exceeded" : remaining < args.maxCost * 0.2 ? "warning" : "active", - }; - await logToolCall( - "omniroute_set_budget_guard", - { maxCost: args.maxCost, action: args.action }, - result, - Date.now() - start, - true - ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_set_budget_guard", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleSetRoutingStrategy(args) { - const start = Date.now(); - try { - const combos = normalizeCombosResponse(await apiFetch("/api/combos")); - const combo = combos.find( - (comboEntry) => - toString(comboEntry.id) === args.comboId || toString(comboEntry.name) === args.comboId - ); - if (!combo) { - const msg = `Combo '${args.comboId}' not found`; - await logToolCall( - "omniroute_set_routing_strategy", - args, - null, - Date.now() - start, - false, - msg - ); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } - const comboId = toString(combo.id); - if (!comboId) { - const msg = "Matched combo has no id"; - await logToolCall( - "omniroute_set_routing_strategy", - args, - null, - Date.now() - start, - false, - msg - ); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } - const comboData = toRecord(combo.data); - const currentConfig = toRecord( - Object.keys(toRecord(combo.config)).length > 0 ? combo.config : comboData.config - ); - let nextConfig = void 0; - if (args.strategy === "auto" && args.autoRoutingStrategy) { - const currentAutoConfig = toRecord(currentConfig.auto); - nextConfig = { - ...currentConfig, - auto: { - ...currentAutoConfig, - routingStrategy: args.autoRoutingStrategy, - }, - }; - } - const payload = { strategy: args.strategy }; - if (nextConfig && Object.keys(nextConfig).length > 0) { - payload.config = nextConfig; - } - const updatedCombo = toRecord( - await apiFetch(`/api/combos/${encodeURIComponent(comboId)}`, { - method: "PUT", - body: JSON.stringify(payload), - }) - ); - const updatedConfig = toRecord(updatedCombo.config); - const resolvedAutoStrategy = - toString(toRecord(updatedConfig.auto).routingStrategy) || - (args.strategy === "auto" ? (args.autoRoutingStrategy ?? "rules") : ""); - const result = { - success: true, - combo: { - id: toString(updatedCombo.id, comboId), - name: toString(updatedCombo.name, toString(combo.name, comboId)), - strategy: toString(updatedCombo.strategy, args.strategy), - autoRoutingStrategy: - toString(updatedCombo.strategy, args.strategy) === "auto" ? resolvedAutoStrategy : null, - }, - }; - await logToolCall("omniroute_set_routing_strategy", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_set_routing_strategy", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleSetResilienceProfile(args) { - const start = Date.now(); - try { - const settings = RESILIENCE_PROFILES[args.profile]; - if (!settings) { - return { - content: [{ type: "text", text: `Error: Invalid profile "${args.profile}"` }], - isError: true, - }; - } - await apiFetch("/api/resilience", { - method: "PATCH", - body: JSON.stringify({ - profiles: settings.profiles, - defaults: settings.defaults, - }), - }); - const result = { applied: true, profile: args.profile, settings }; - await logToolCall("omniroute_set_resilience_profile", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall( - "omniroute_set_resilience_profile", - args, - null, - Date.now() - start, - false, - msg - ); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleTestCombo(args) { - const start = Date.now(); - try { - const combos = normalizeCombosResponse(await apiFetch("/api/combos")); - const combo = combos.find( - (comboEntry) => - toString(comboEntry.id) === args.comboId || toString(comboEntry.name) === args.comboId - ); - if (!combo) { - return { - content: [ - { - type: "text", - text: JSON.stringify({ error: `Combo "${args.comboId}" not found` }), - }, - ], - isError: true, - }; - } - const models = getComboModels(combo); - const prompt = (args.testPrompt || "Say hello").slice(0, 200); - const results = await Promise.allSettled( - models.map(async (model) => { - const providerStart = Date.now(); - try { - const resp = toRecord( - await apiFetch("/v1/chat/completions", { - method: "POST", - body: JSON.stringify({ - model: model.model || "auto", - messages: [{ role: "user", content: prompt }], - max_tokens: 50, - stream: false, - "x-provider": model.provider, - }), - }) - ); - const usage = toRecord(resp.usage); - return { - provider: model.provider, - model: model.model || toString(resp.model, "unknown"), - success: true, - latencyMs: Date.now() - providerStart, - cost: toNumber2(resp.cost, 0), - tokenCount: toNumber2(usage.prompt_tokens, 0) + toNumber2(usage.completion_tokens, 0), - }; - } catch (err) { - return { - provider: model.provider, - model: model.model || "unknown", - success: false, - latencyMs: Date.now() - providerStart, - cost: 0, - tokenCount: 0, - error: err instanceof Error ? err.message : String(err), - }; - } - }) - ); - const providerResults = results.map((r) => - r.status === "fulfilled" - ? r.value - : { - provider: "unknown", - model: "unknown", - success: false, - latencyMs: 0, - cost: 0, - tokenCount: 0, - error: "Promise rejected", - } - ); - const successful = providerResults.filter((r) => r.success); - const fastest = successful.sort((a, b) => a.latencyMs - b.latencyMs)[0]; - const cheapest = successful.sort((a, b) => a.cost - b.cost)[0]; - const result = { - results: providerResults, - summary: { - totalProviders: providerResults.length, - successful: successful.length, - fastestProvider: fastest?.provider || "none", - cheapestProvider: cheapest?.provider || "none", - }, - }; - await logToolCall( - "omniroute_test_combo", - { comboId: args.comboId }, - result.summary, - Date.now() - start, - true - ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_test_combo", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleGetProviderMetrics(args) { - const start = Date.now(); - try { - const [healthRaw, quotaRaw, analyticsRaw] = await Promise.allSettled([ - apiFetch("/api/monitoring/health"), - apiFetch(`/api/usage/quota?provider=${encodeURIComponent(args.provider)}`), - apiFetch(`/api/usage/analytics?period=session&provider=${encodeURIComponent(args.provider)}`), - ]); - const health = healthRaw.status === "fulfilled" ? toRecord(healthRaw.value) : {}; - const quota = - quotaRaw.status === "fulfilled" - ? normalizeQuotaResponse(quotaRaw.value, { provider: args.provider }) - : normalizeQuotaResponse({}); - const analytics = analyticsRaw.status === "fulfilled" ? toRecord(analyticsRaw.value) : {}; - const cb = toArrayOfRecords(health.circuitBreakers).find( - (breaker) => toString(breaker.provider) === args.provider - ); - const providerQuota = quota.providers.find((p) => p.provider === args.provider) || null; - const result = { - provider: args.provider, - successRate: toNumber2(analytics.successRate, 1), - requestCount: toNumber2(analytics.requestCount, 0), - avgLatencyMs: toNumber2(analytics.avgLatencyMs, 0), - p50LatencyMs: toNumber2(analytics.p50LatencyMs, 0), - p95LatencyMs: toNumber2(analytics.p95LatencyMs, 0), - p99LatencyMs: toNumber2(analytics.p99LatencyMs, 0), - errorRate: toNumber2(analytics.errorRate, 0), - lastError: toString(analytics.lastError) || null, - circuitBreakerState: toString(cb?.state, "CLOSED"), - quotaInfo: providerQuota - ? { - used: providerQuota.quotaUsed, - total: providerQuota.quotaTotal, - resetAt: providerQuota.resetAt, - } - : { used: 0, total: null, resetAt: null }, - }; - await logToolCall("omniroute_get_provider_metrics", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_get_provider_metrics", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleBestComboForTask(args) { - const start = Date.now(); - try { - const fitness = TASK_FITNESS[args.taskType] || TASK_FITNESS.coding; - const combos = normalizeCombosResponse(await apiFetch("/api/combos")); - const enabledCombos = combos.filter((combo) => combo.enabled !== false); - if (enabledCombos.length === 0) { - return { - content: [{ type: "text", text: JSON.stringify({ error: "No enabled combos available" }) }], - isError: true, - }; - } - const scored = enabledCombos.map((combo) => { - const models = getComboModels(combo); - let score = 0; - for (const model of models) { - const prefIdx = fitness.preferred.indexOf(model.provider); - if (prefIdx >= 0) score += (fitness.preferred.length - prefIdx) * 10; - } - const name = toString(combo.name).toLowerCase(); - for (const trait of fitness.traits) { - if (name.includes(trait)) score += 5; - } - const isFree = - name.includes("free") || - models.every((model) => model.provider.toLowerCase().includes("free")); - return { combo, score, isFree }; - }); - scored.sort((a, b) => b.score - a.score); - const best = scored[0]; - const alternatives = scored.slice(1, 4).map((s) => ({ - id: s.combo.id, - name: s.combo.name, - tradeoff: s.isFree - ? "free but may have limits" - : s.score < best.score * 0.5 - ? "cheaper but slower" - : "similar quality, different providers", - })); - const freeAlt = scored.find((s) => s.isFree && s !== best); - const result = { - recommendedCombo: { - id: best.combo.id, - name: best.combo.name, - reason: `Best match for "${args.taskType}": preferred providers (${fitness.preferred.slice(0, 3).join(", ")})`, - }, - alternatives, - freeAlternative: freeAlt ? { id: freeAlt.combo.id, name: freeAlt.combo.name } : null, - }; - await logToolCall( - "omniroute_best_combo_for_task", - args, - result.recommendedCombo, - Date.now() - start, - true - ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_best_combo_for_task", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleExplainRoute(args) { - const start = Date.now(); - try { - let decision = null; - try { - decision = toRecord( - await apiFetch(`/api/routing/decisions/${encodeURIComponent(args.requestId)}`) - ); - } catch {} - const result = decision - ? { - requestId: args.requestId, - decision: { - comboUsed: decision.comboUsed || "default", - providerSelected: decision.providerSelected || "unknown", - modelUsed: decision.modelUsed || "unknown", - score: decision.score || 0, - factors: decision.factors || [ - { name: "health", value: 1, weight: 0.3, contribution: 0.3 }, - { name: "quota", value: 1, weight: 0.25, contribution: 0.25 }, - { name: "cost", value: 0.8, weight: 0.2, contribution: 0.16 }, - { name: "latency", value: 0.9, weight: 0.15, contribution: 0.135 }, - { name: "task_fit", value: 0.7, weight: 0.1, contribution: 0.07 }, - ], - fallbacksTriggered: decision.fallbacksTriggered || [], - costActual: decision.costActual || 0, - latencyActual: decision.latencyActual || 0, - }, - } - : { - requestId: args.requestId, - decision: { - comboUsed: "unknown", - providerSelected: "unknown", - modelUsed: "unknown", - score: 0, - factors: [], - fallbacksTriggered: [], - costActual: 0, - latencyActual: 0, - }, - note: "Routing decision not found. The /api/routing/decisions endpoint may not be implemented yet, or the requestId is invalid.", - }; - await logToolCall( - "omniroute_explain_route", - args, - { requestId: args.requestId }, - Date.now() - start, - true - ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_explain_route", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleSyncPricing(args) { - const start = Date.now(); - try { - const result = toRecord( - await apiFetch("/api/pricing/sync", { - method: "POST", - body: JSON.stringify({ - sources: args.sources, - dryRun: args.dryRun ?? false, - }), - }) - ); - await logToolCall("omniroute_sync_pricing", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_sync_pricing", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleGetSessionSnapshot() { - const start = Date.now(); - try { - const analytics = toRecord( - await apiFetch("/api/usage/analytics?period=session").catch(() => ({})) - ); - const tokenCount = toRecord(analytics.tokenCount); - const byModel = toArrayOfRecords(analytics.byModel); - const byProvider = toArrayOfRecords(analytics.byProvider); - const result = { - sessionStart: toString(analytics.sessionStart, /* @__PURE__ */ new Date().toISOString()), - duration: toString(analytics.duration, "unknown"), - requestCount: toNumber2(analytics.requestCount, 0), - costTotal: toNumber2(analytics.totalCost, 0), - tokenCount: { - prompt: toNumber2(tokenCount.prompt, 0), - completion: toNumber2(tokenCount.completion, 0), - }, - topModels: byModel.slice(0, 5).map((model) => ({ - model: toString(model.model, "unknown"), - count: toNumber2(model.requests, 0), - })), - topProviders: byProvider.slice(0, 5).map((provider) => ({ - provider: toString(provider.name, "unknown"), - count: toNumber2(provider.requests, 0), - })), - errors: toNumber2(analytics.errorCount, 0), - fallbacks: toNumber2(analytics.fallbackCount, 0), - budgetGuard: activeBudgetGuard - ? { - active: true, - remaining: Math.max(0, activeBudgetGuard.maxCost - activeBudgetGuard.spent), - action: activeBudgetGuard.action, - } - : null, - }; - await logToolCall( - "omniroute_get_session_snapshot", - {}, - { requestCount: result.requestCount }, - Date.now() - start, - true - ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_get_session_snapshot", {}, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} - -// open-sse/mcp-server/tools/memoryTools.ts -import { z as z3 } from "zod"; - -// src/lib/db/core.ts -import Database from "better-sqlite3"; -import path3 from "path"; -import fs3 from "fs"; - -// src/lib/dataPaths.ts -import path from "path"; -import os from "os"; -var APP_NAME = "omniroute"; -function safeHomeDir() { - try { - return os.homedir(); - } catch { - return process.cwd(); - } -} -function normalizeConfiguredPath(dir) { - if (typeof dir !== "string") return null; - const trimmed = dir.trim(); - if (!trimmed) return null; - return path.resolve(trimmed); -} -function getLegacyDotDataDir() { - return path.join(safeHomeDir(), `.${APP_NAME}`); -} -function getDefaultDataDir() { - const homeDir = safeHomeDir(); - if (process.platform === "win32") { - const appData = process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"); - return path.join(appData, APP_NAME); - } - const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME); - if (xdgConfigHome) { - return path.join(xdgConfigHome, APP_NAME); - } - return getLegacyDotDataDir(); -} -function resolveDataDir2({ isCloud: isCloud2 = false } = {}) { - if (isCloud2) return "/tmp"; - const configured = normalizeConfiguredPath(process.env.DATA_DIR); - if (configured) return configured; - return getDefaultDataDir(); -} - -// src/lib/db/migrationRunner.ts -import fs2 from "fs"; -import path2 from "path"; -import { fileURLToPath } from "url"; -function resolveMigrationsDir() { - try { - const metaUrl = import.meta.url; - if (metaUrl && metaUrl.startsWith("file://")) { - const __filename = fileURLToPath(metaUrl); - return path2.join(path2.dirname(__filename), "migrations"); - } - } catch {} - return path2.join(process.cwd(), "src", "lib", "db", "migrations"); -} -var MIGRATIONS_DIR = resolveMigrationsDir(); -function ensureMigrationsTable(db2) { - db2.exec(` - CREATE TABLE IF NOT EXISTS _omniroute_migrations ( - version TEXT PRIMARY KEY, - name TEXT NOT NULL, - applied_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - `); -} -function getMigrationFiles() { - if (!fs2.existsSync(MIGRATIONS_DIR)) return []; - return fs2 - .readdirSync(MIGRATIONS_DIR) - .filter((f) => f.endsWith(".sql")) - .sort() - .map((filename) => { - const match = filename.match(/^(\d+)_(.+)\.sql$/); - if (!match) return null; - return { - version: match[1], - name: match[2], - path: path2.join(MIGRATIONS_DIR, filename), - }; - }) - .filter(Boolean); -} -function getAppliedVersions(db2) { - const rows = db2.prepare("SELECT version FROM _omniroute_migrations").all(); - return new Set(rows.map((r) => r.version)); -} -function runMigrations(db2) { - ensureMigrationsTable(db2); - const files = getMigrationFiles(); - const applied = getAppliedVersions(db2); - let count = 0; - for (const migration of files) { - if (applied.has(migration.version)) continue; - const sql = fs2.readFileSync(migration.path, "utf-8"); - const applyMigration = db2.transaction(() => { - db2.exec(sql); - db2 - .prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)") - .run(migration.version, migration.name); - }); - try { - applyMigration(); - count++; - console.log(`[Migration] Applied: ${migration.version}_${migration.name}`); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error(`[Migration] FAILED: ${migration.version}_${migration.name} \u2014 ${message}`); - throw err; - } - } - if (count > 0) { - console.log(`[Migration] ${count} migration(s) applied successfully.`); - } - return count; -} - -// src/lib/db/core.ts -var isCloud = typeof globalThis.caches === "object" && globalThis.caches !== null; -var isBuildPhase = process.env.NEXT_PHASE === "phase-production-build"; -var DATA_DIR = resolveDataDir2({ isCloud }); -var LEGACY_DATA_DIR = isCloud ? null : getLegacyDotDataDir(); -var SQLITE_FILE = isCloud ? null : path3.join(DATA_DIR, "storage.sqlite"); -var JSON_DB_FILE = isCloud ? null : path3.join(DATA_DIR, "db.json"); -var DB_BACKUPS_DIR = isCloud ? null : path3.join(DATA_DIR, "db_backups"); -if (!isCloud && !fs3.existsSync(DATA_DIR)) { - try { - fs3.mkdirSync(DATA_DIR, { recursive: true }); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - console.warn( - `[DB] Cannot create data directory '${DATA_DIR}': ${msg} -[DB] Set the DATA_DIR environment variable to a writable path, e.g.: -[DB] DATA_DIR=/path/to/writable/dir omniroute` - ); - } -} -var SCHEMA_SQL = ` - CREATE TABLE IF NOT EXISTS provider_connections ( - id TEXT PRIMARY KEY, - provider TEXT NOT NULL, - auth_type TEXT, - name TEXT, - email TEXT, - priority INTEGER DEFAULT 0, - is_active INTEGER DEFAULT 1, - access_token TEXT, - refresh_token TEXT, - expires_at TEXT, - token_expires_at TEXT, - scope TEXT, - project_id TEXT, - test_status TEXT, - error_code TEXT, - last_error TEXT, - last_error_at TEXT, - last_error_type TEXT, - last_error_source TEXT, - backoff_level INTEGER DEFAULT 0, - rate_limited_until TEXT, - health_check_interval INTEGER, - last_health_check_at TEXT, - last_tested TEXT, - api_key TEXT, - id_token TEXT, - provider_specific_data TEXT, - expires_in INTEGER, - display_name TEXT, - global_priority INTEGER, - default_model TEXT, - token_type TEXT, - consecutive_use_count INTEGER DEFAULT 0, - rate_limit_protection INTEGER DEFAULT 0, - last_used_at TEXT, - "group" TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_pc_provider ON provider_connections(provider); - CREATE INDEX IF NOT EXISTS idx_pc_active ON provider_connections(is_active); - CREATE INDEX IF NOT EXISTS idx_pc_priority ON provider_connections(provider, priority); - - CREATE TABLE IF NOT EXISTS provider_nodes ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL, - name TEXT NOT NULL, - prefix TEXT, - api_type TEXT, - base_url TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS key_value ( - namespace TEXT NOT NULL, - key TEXT NOT NULL, - value TEXT NOT NULL, - PRIMARY KEY (namespace, key) - ); - - CREATE TABLE IF NOT EXISTS combos ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL UNIQUE, - data TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS api_keys ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - key TEXT NOT NULL UNIQUE, - machine_id TEXT, - allowed_models TEXT DEFAULT '[]', - no_log INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_ak_key ON api_keys(key); - - CREATE TABLE IF NOT EXISTS db_meta ( - key TEXT PRIMARY KEY, - value TEXT - ); - - CREATE TABLE IF NOT EXISTS usage_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - provider TEXT, - model TEXT, - connection_id TEXT, - api_key_id TEXT, - api_key_name TEXT, - tokens_input INTEGER DEFAULT 0, - tokens_output INTEGER DEFAULT 0, - tokens_cache_read INTEGER DEFAULT 0, - tokens_cache_creation INTEGER DEFAULT 0, - tokens_reasoning INTEGER DEFAULT 0, - status TEXT, - success INTEGER DEFAULT 1, - latency_ms INTEGER DEFAULT 0, - ttft_ms INTEGER DEFAULT 0, - error_code TEXT, - timestamp TEXT NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_uh_timestamp ON usage_history(timestamp); - CREATE INDEX IF NOT EXISTS idx_uh_provider ON usage_history(provider); - CREATE INDEX IF NOT EXISTS idx_uh_model ON usage_history(model); - - CREATE TABLE IF NOT EXISTS call_logs ( - id TEXT PRIMARY KEY, - timestamp TEXT NOT NULL, - method TEXT, - path TEXT, - status INTEGER, - model TEXT, - provider TEXT, - account TEXT, - connection_id TEXT, - duration INTEGER DEFAULT 0, - tokens_in INTEGER DEFAULT 0, - tokens_out INTEGER DEFAULT 0, - source_format TEXT, - target_format TEXT, - api_key_id TEXT, - api_key_name TEXT, - combo_name TEXT, - request_body TEXT, - response_body TEXT, - error TEXT, - artifact_relpath TEXT, - has_pipeline_details INTEGER DEFAULT 0 - ); - CREATE INDEX IF NOT EXISTS idx_cl_timestamp ON call_logs(timestamp); - CREATE INDEX IF NOT EXISTS idx_cl_status ON call_logs(status); - - CREATE TABLE IF NOT EXISTS proxy_logs ( - id TEXT PRIMARY KEY, - timestamp TEXT NOT NULL, - status TEXT, - proxy_type TEXT, - proxy_host TEXT, - proxy_port INTEGER, - level TEXT, - level_id TEXT, - provider TEXT, - target_url TEXT, - public_ip TEXT, - latency_ms INTEGER DEFAULT 0, - error TEXT, - connection_id TEXT, - combo_id TEXT, - account TEXT, - tls_fingerprint INTEGER DEFAULT 0 - ); - CREATE INDEX IF NOT EXISTS idx_pl_timestamp ON proxy_logs(timestamp); - CREATE INDEX IF NOT EXISTS idx_pl_status ON proxy_logs(status); - CREATE INDEX IF NOT EXISTS idx_pl_provider ON proxy_logs(provider); - - -- Domain State Persistence (Phase 5) - CREATE TABLE IF NOT EXISTS domain_fallback_chains ( - model TEXT PRIMARY KEY, - chain TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS domain_budgets ( - api_key_id TEXT PRIMARY KEY, - daily_limit_usd REAL NOT NULL, - monthly_limit_usd REAL DEFAULT 0, - warning_threshold REAL DEFAULT 0.8 - ); - - CREATE TABLE IF NOT EXISTS domain_cost_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - api_key_id TEXT NOT NULL, - cost REAL NOT NULL, - timestamp INTEGER NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_dch_key ON domain_cost_history(api_key_id); - CREATE INDEX IF NOT EXISTS idx_dch_ts ON domain_cost_history(timestamp); - - CREATE TABLE IF NOT EXISTS domain_lockout_state ( - identifier TEXT PRIMARY KEY, - attempts TEXT NOT NULL, - locked_until INTEGER - ); - - CREATE TABLE IF NOT EXISTS domain_circuit_breakers ( - name TEXT PRIMARY KEY, - state TEXT NOT NULL DEFAULT 'CLOSED', - failure_count INTEGER DEFAULT 0, - last_failure_time INTEGER, - options TEXT - ); - - CREATE TABLE IF NOT EXISTS semantic_cache ( - id TEXT PRIMARY KEY, - signature TEXT NOT NULL UNIQUE, - model TEXT NOT NULL, - prompt_hash TEXT NOT NULL, - response TEXT NOT NULL, - tokens_saved INTEGER DEFAULT 0, - hit_count INTEGER DEFAULT 0, - created_at TEXT NOT NULL, - expires_at TEXT NOT NULL - ); - CREATE INDEX IF NOT EXISTS idx_sc_sig ON semantic_cache(signature); - CREATE INDEX IF NOT EXISTS idx_sc_model ON semantic_cache(model); -`; -function getDb2() { - return globalThis.__omnirouteDb ?? null; -} -function setDb(db2) { - if (db2) { - globalThis.__omnirouteDb = db2; - } else { - delete globalThis.__omnirouteDb; - } -} -function ensureProviderConnectionsColumns(db2) { - try { - const columns = db2.prepare("PRAGMA table_info(provider_connections)").all(); - const columnNames = new Set(columns.map((column) => String(column.name ?? ""))); - if (!columnNames.has("rate_limit_protection")) { - db2.exec( - "ALTER TABLE provider_connections ADD COLUMN rate_limit_protection INTEGER DEFAULT 0" - ); - console.log("[DB] Added provider_connections.rate_limit_protection column"); - } - if (!columnNames.has("last_used_at")) { - db2.exec("ALTER TABLE provider_connections ADD COLUMN last_used_at TEXT"); - console.log("[DB] Added provider_connections.last_used_at column"); - } - if (!columnNames.has("group")) { - db2.exec('ALTER TABLE provider_connections ADD COLUMN "group" TEXT'); - console.log('[DB] Added provider_connections."group" column'); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn("[DB] Failed to verify provider_connections schema:", message); - } -} -function ensureUsageHistoryColumns(db2) { - try { - const columns = db2.prepare("PRAGMA table_info(usage_history)").all(); - const columnNames = new Set(columns.map((column) => String(column.name ?? ""))); - if (!columnNames.has("success")) { - db2.exec("ALTER TABLE usage_history ADD COLUMN success INTEGER DEFAULT 1"); - console.log("[DB] Added usage_history.success column"); - } - if (!columnNames.has("latency_ms")) { - db2.exec("ALTER TABLE usage_history ADD COLUMN latency_ms INTEGER DEFAULT 0"); - console.log("[DB] Added usage_history.latency_ms column"); - } - if (!columnNames.has("ttft_ms")) { - db2.exec("ALTER TABLE usage_history ADD COLUMN ttft_ms INTEGER DEFAULT 0"); - console.log("[DB] Added usage_history.ttft_ms column"); - } - if (!columnNames.has("error_code")) { - db2.exec("ALTER TABLE usage_history ADD COLUMN error_code TEXT"); - console.log("[DB] Added usage_history.error_code column"); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn("[DB] Failed to verify usage_history schema:", message); - } -} -function ensureCallLogsColumns(db2) { - try { - const columns = db2.prepare("PRAGMA table_info(call_logs)").all(); - const columnNames = new Set(columns.map((column) => String(column.name ?? ""))); - if (!columnNames.has("artifact_relpath")) { - db2.exec("ALTER TABLE call_logs ADD COLUMN artifact_relpath TEXT"); - console.log("[DB] Added call_logs.artifact_relpath column"); - } - if (!columnNames.has("has_pipeline_details")) { - db2.exec("ALTER TABLE call_logs ADD COLUMN has_pipeline_details INTEGER DEFAULT 0"); - console.log("[DB] Added call_logs.has_pipeline_details column"); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn("[DB] Failed to verify call_logs schema:", message); - } -} -function getDbInstance() { - const existing = getDb2(); - if (existing) return existing; - if (isCloud || isBuildPhase) { - if (isBuildPhase) { - console.log("[DB] Build phase detected \u2014 using in-memory SQLite (read-only)"); - } - const memoryDb = new Database(":memory:"); - memoryDb.pragma("journal_mode = WAL"); - memoryDb.exec(SCHEMA_SQL); - ensureUsageHistoryColumns(memoryDb); - setDb(memoryDb); - return memoryDb; - } - const sqliteFile = SQLITE_FILE; - if (!sqliteFile) { - throw new Error("SQLITE_FILE is unavailable for local mode"); - } - const jsonDbFile = JSON_DB_FILE; - if (fs3.existsSync(sqliteFile)) { - try { - const probe = new Database(sqliteFile, { readonly: true }); - const hasOldSchema = probe - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'") - .get(); - if (hasOldSchema) { - let hasData = false; - try { - const count = probe.prepare("SELECT COUNT(*) as c FROM provider_connections").get(); - hasData = Boolean(count && count.c > 0); - } catch {} - probe.close(); - if (hasData) { - console.log( - `[DB] Old schema_migrations table found but data exists \u2014 preserving data (#146)` - ); - const fixDb = new Database(sqliteFile); - try { - fixDb.exec("DROP TABLE IF EXISTS schema_migrations"); - fixDb.pragma("wal_checkpoint(TRUNCATE)"); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - console.warn("[DB] Could not clean up old schema table:", message); - } finally { - fixDb.close(); - } - } else { - const oldPath = sqliteFile + ".old-schema"; - console.log( - `[DB] Old incompatible schema detected (empty) \u2014 renaming to ${path3.basename(oldPath)}` - ); - fs3.renameSync(sqliteFile, oldPath); - for (const ext of ["-wal", "-shm"]) { - try { - if (fs3.existsSync(sqliteFile + ext)) fs3.unlinkSync(sqliteFile + ext); - } catch {} - } - } - } else { - probe.close(); - } - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - console.warn("[DB] Could not probe existing DB, will create fresh:", message); - try { - fs3.unlinkSync(sqliteFile); - } catch {} - } - } - const db2 = new Database(sqliteFile); - db2.pragma("journal_mode = WAL"); - db2.pragma("busy_timeout = 5000"); - db2.pragma("synchronous = NORMAL"); - db2.exec(SCHEMA_SQL); - ensureProviderConnectionsColumns(db2); - ensureUsageHistoryColumns(db2); - ensureCallLogsColumns(db2); - db2.exec(` - CREATE TABLE IF NOT EXISTS _omniroute_migrations ( - version TEXT PRIMARY KEY, - name TEXT NOT NULL, - applied_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - INSERT OR IGNORE INTO _omniroute_migrations (version, name) - VALUES ('001', 'initial_schema'); - `); - runMigrations(db2); - if (jsonDbFile && fs3.existsSync(jsonDbFile)) { - migrateFromJson(db2, jsonDbFile); - } - const versionStmt = db2.prepare( - "INSERT OR REPLACE INTO db_meta (key, value) VALUES ('schema_version', '1')" - ); - versionStmt.run(); - setDb(db2); - console.log(`[DB] SQLite database ready: ${sqliteFile}`); - return db2; -} -function migrateFromJson(db2, jsonPath) { - try { - const raw = fs3.readFileSync(jsonPath, "utf-8"); - const data = JSON.parse(raw); - const connCount = (data.providerConnections || []).length; - const nodeCount = (data.providerNodes || []).length; - const keyCount = (data.apiKeys || []).length; - if (connCount === 0 && nodeCount === 0 && keyCount === 0) { - console.log("[DB] db.json has no data to migrate, skipping"); - fs3.renameSync(jsonPath, jsonPath + ".empty"); - return; - } - console.log( - `[DB] Migrating db.json \u2192 SQLite (${connCount} connections, ${nodeCount} nodes, ${keyCount} keys)...` - ); - const migrate = db2.transaction(() => { - const insertConn = db2.prepare(` - INSERT OR REPLACE INTO provider_connections ( - id, provider, auth_type, name, email, priority, is_active, - access_token, refresh_token, expires_at, token_expires_at, - scope, project_id, test_status, error_code, last_error, - last_error_at, last_error_type, last_error_source, backoff_level, - rate_limited_until, health_check_interval, last_health_check_at, - last_tested, api_key, id_token, provider_specific_data, - expires_in, display_name, global_priority, default_model, - token_type, consecutive_use_count, rate_limit_protection, last_used_at, created_at, updated_at - ) VALUES ( - @id, @provider, @authType, @name, @email, @priority, @isActive, - @accessToken, @refreshToken, @expiresAt, @tokenExpiresAt, - @scope, @projectId, @testStatus, @errorCode, @lastError, - @lastErrorAt, @lastErrorType, @lastErrorSource, @backoffLevel, - @rateLimitedUntil, @healthCheckInterval, @lastHealthCheckAt, - @lastTested, @apiKey, @idToken, @providerSpecificData, - @expiresIn, @displayName, @globalPriority, @defaultModel, - @tokenType, @consecutiveUseCount, @rateLimitProtection, @lastUsedAt, @createdAt, @updatedAt - ) - `); - for (const conn of data.providerConnections || []) { - insertConn.run({ - id: conn.id, - provider: conn.provider, - authType: conn.authType || "oauth", - name: conn.name || null, - email: conn.email || null, - priority: conn.priority || 0, - isActive: conn.isActive === false ? 0 : 1, - accessToken: conn.accessToken || null, - refreshToken: conn.refreshToken || null, - expiresAt: conn.expiresAt || null, - tokenExpiresAt: conn.tokenExpiresAt || null, - scope: conn.scope || null, - projectId: conn.projectId || null, - testStatus: conn.testStatus || null, - errorCode: conn.errorCode || null, - lastError: conn.lastError || null, - lastErrorAt: conn.lastErrorAt || null, - lastErrorType: conn.lastErrorType || null, - lastErrorSource: conn.lastErrorSource || null, - backoffLevel: conn.backoffLevel || 0, - rateLimitedUntil: conn.rateLimitedUntil || null, - healthCheckInterval: conn.healthCheckInterval || null, - lastHealthCheckAt: conn.lastHealthCheckAt || null, - lastTested: conn.lastTested || null, - apiKey: conn.apiKey || null, - idToken: conn.idToken || null, - providerSpecificData: conn.providerSpecificData - ? JSON.stringify(conn.providerSpecificData) - : null, - expiresIn: conn.expiresIn || null, - displayName: conn.displayName || null, - globalPriority: conn.globalPriority || null, - defaultModel: conn.defaultModel || null, - tokenType: conn.tokenType || null, - consecutiveUseCount: conn.consecutiveUseCount || 0, - lastUsedAt: conn.lastUsedAt || null, - rateLimitProtection: - conn.rateLimitProtection === true || conn.rateLimitProtection === 1 ? 1 : 0, - createdAt: conn.createdAt || /* @__PURE__ */ new Date().toISOString(), - updatedAt: conn.updatedAt || /* @__PURE__ */ new Date().toISOString(), - }); - } - const insertNode = db2.prepare(` - INSERT OR REPLACE INTO provider_nodes (id, type, name, prefix, api_type, base_url, created_at, updated_at) - VALUES (@id, @type, @name, @prefix, @apiType, @baseUrl, @createdAt, @updatedAt) - `); - for (const node of data.providerNodes || []) { - insertNode.run({ - id: node.id, - type: node.type, - name: node.name, - prefix: node.prefix || null, - apiType: node.apiType || null, - baseUrl: node.baseUrl || null, - createdAt: node.createdAt || /* @__PURE__ */ new Date().toISOString(), - updatedAt: node.updatedAt || /* @__PURE__ */ new Date().toISOString(), - }); - } - const insertKv = db2.prepare( - "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)" - ); - for (const [alias, model] of Object.entries(data.modelAliases || {})) { - insertKv.run("modelAliases", alias, JSON.stringify(model)); - } - for (const [toolName, mappings] of Object.entries(data.mitmAlias || {})) { - insertKv.run("mitmAlias", toolName, JSON.stringify(mappings)); - } - for (const [key, value] of Object.entries(data.settings || {})) { - insertKv.run("settings", key, JSON.stringify(value)); - } - for (const [provider, models] of Object.entries(data.pricing || {})) { - insertKv.run("pricing", provider, JSON.stringify(models)); - } - for (const [providerId, models] of Object.entries(data.customModels || {})) { - insertKv.run("customModels", providerId, JSON.stringify(models)); - } - if (data.proxyConfig) { - insertKv.run("proxyConfig", "global", JSON.stringify(data.proxyConfig.global || null)); - insertKv.run("proxyConfig", "providers", JSON.stringify(data.proxyConfig.providers || {})); - insertKv.run("proxyConfig", "combos", JSON.stringify(data.proxyConfig.combos || {})); - insertKv.run("proxyConfig", "keys", JSON.stringify(data.proxyConfig.keys || {})); - } - const insertCombo = db2.prepare(` - INSERT OR REPLACE INTO combos (id, name, data, created_at, updated_at) - VALUES (@id, @name, @data, @createdAt, @updatedAt) - `); - for (const combo of data.combos || []) { - insertCombo.run({ - id: combo.id, - name: combo.name, - data: JSON.stringify(combo), - createdAt: combo.createdAt || /* @__PURE__ */ new Date().toISOString(), - updatedAt: combo.updatedAt || /* @__PURE__ */ new Date().toISOString(), - }); - } - const insertKey = db2.prepare(` - INSERT OR REPLACE INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at) - VALUES (@id, @name, @key, @machineId, @allowedModels, @noLog, @createdAt) - `); - for (const apiKey of data.apiKeys || []) { - insertKey.run({ - id: apiKey.id, - name: apiKey.name, - key: apiKey.key, - machineId: apiKey.machineId || null, - allowedModels: JSON.stringify(apiKey.allowedModels || []), - noLog: apiKey.noLog ? 1 : 0, - createdAt: apiKey.createdAt || /* @__PURE__ */ new Date().toISOString(), - }); - } - }); - migrate(); - const migratedPath = jsonPath + ".migrated"; - fs3.renameSync(jsonPath, migratedPath); - console.log(`[DB] \u2713 Migration complete. Original saved as ${migratedPath}`); - const legacyBackupDir = path3.join(DATA_DIR, "db_backups"); - if (fs3.existsSync(legacyBackupDir)) { - const jsonBackups = fs3.readdirSync(legacyBackupDir).filter((f) => f.endsWith(".json")); - if (jsonBackups.length > 0) { - console.log( - `[DB] Note: ${jsonBackups.length} legacy .json backups remain in ${legacyBackupDir}` - ); - } - } - } catch (err) { - console.error("[DB] Migration from db.json failed:", err.message); - } -} - -// src/lib/memory/schemas.ts -import { z as z2 } from "zod"; - -// src/lib/memory/types.ts -var MemoryType = /* @__PURE__ */ ((MemoryType2) => { - MemoryType2["FACTUAL"] = "factual"; - MemoryType2["EPISODIC"] = "episodic"; - MemoryType2["PROCEDURAL"] = "procedural"; - MemoryType2["SEMANTIC"] = "semantic"; - return MemoryType2; -})(MemoryType || {}); - -// src/lib/memory/schemas.ts -var MemoryConfigSchema = z2.object({ - enabled: z2.boolean(), - maxTokens: z2.number().int().positive(), - retrievalStrategy: z2.enum(["exact", "semantic", "hybrid"]).optional(), - autoSummarize: z2.boolean(), - persistAcrossModels: z2.boolean(), - retentionDays: z2.number().int().positive(), - scope: z2.enum(["session", "apiKey", "global"]).optional(), -}); -var MemoryCreateInputSchema = z2 - .object({ - type: z2.nativeEnum(MemoryType), - key: z2.string().min(1), - content: z2.string().min(1), - metadata: z2.record(z2.string(), z2.unknown()).optional(), - }) - .strict(); -var MemoryUpdateInputSchema = z2 - .object({ - type: z2.nativeEnum(MemoryType).optional(), - key: z2.string().min(1).optional(), - content: z2.string().min(1).optional(), - metadata: z2.record(z2.string(), z2.unknown()).optional(), - }) - .strict(); - -// src/lib/memory/retrieval.ts -function estimateTokens(text) { - if (!text || typeof text !== "string") return 0; - return Math.ceil(text.length / 4); -} -async function retrieveMemories(apiKeyId, config = {}) { - const normalizedConfig = MemoryConfigSchema.parse({ - enabled: true, - maxTokens: 2e3, - retrievalStrategy: "recent", - autoSummarize: false, - persistAcrossModels: false, - retentionDays: 30, - scope: "apiKey", - ...config, - }); - const maxTokens = Math.min(Math.max(normalizedConfig.maxTokens, 100), 8e3); - const strategy = normalizedConfig.retrievalStrategy; - const db2 = getDbInstance(); - const memories = []; - let totalTokens = 0; - let query = "SELECT * FROM memory WHERE apiKeyId = ?"; - const params = [apiKeyId]; - switch (strategy) { - case "semantic": - query += " ORDER BY createdAt DESC"; - break; - case "hybrid": - query += " ORDER BY createdAt DESC"; - break; - case "exact": - default: - query += " ORDER BY createdAt DESC"; - } - query += " LIMIT 100"; - const stmt = db2.prepare(query); - const rows = stmt.all(...params); - for (const row of rows) { - const memory = { - id: String(row.id), - apiKeyId: String(row.apiKeyId), - sessionId: String(row.sessionId), - type: row.type, - key: String(row.key), - content: String(row.content), - metadata: JSON.parse(String(row.metadata)), - createdAt: new Date(String(row.createdAt)), - updatedAt: new Date(String(row.updatedAt)), - expiresAt: row.expiresAt ? new Date(String(row.expiresAt)) : null, - }; - const memoryTokens = estimateTokens(memory.content); - if (totalTokens + memoryTokens > maxTokens) { - if (memories.length === 0) { - memories.push(memory); - totalTokens += memoryTokens; - } - break; - } - memories.push(memory); - totalTokens += memoryTokens; - } - return memories; -} - -// src/lib/memory/store.ts -var MEMORY_MAX_CACHE_SIZE = 1e4; -var _memoryCache = /* @__PURE__ */ new Map(); -function parseJSON(value) { - if (!value || typeof value !== "string" || value.trim() === "") { - return {}; - } - try { - const parsed = JSON.parse(value); - return typeof parsed === "object" && parsed !== null ? parsed : {}; - } catch { - return {}; - } -} -function invalidateMemoryCache(key) { - _memoryCache.delete(key); -} -function evictIfNeeded(cache) { - if (cache.size > MEMORY_MAX_CACHE_SIZE) { - const keysArray = Array.from(cache.keys()); - const entriesToRemove = Math.floor(cache.size * 0.2); - for (let i = 0; i < entriesToRemove; i++) { - cache.delete(keysArray[i]); - } - } -} -var MEMORY_VALIDATION_CACHE_TTL = 60 * 1e3; -async function createMemory(memory) { - const db2 = getDbInstance(); - const id = crypto.randomUUID(); - const now = /* @__PURE__ */ new Date().toISOString(); - const stmt = db2.prepare( - "INSERT INTO memory (id, apiKeyId, sessionId, type, key, content, metadata, createdAt, updatedAt, expiresAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - ); - stmt.run( - id, - memory.apiKeyId, - memory.sessionId, - memory.type, - memory.key, - memory.content, - JSON.stringify(memory.metadata), - now, - now, - memory.expiresAt?.toISOString() ?? null - ); - const createdMemory = { - id, - apiKeyId: memory.apiKeyId, - sessionId: memory.sessionId, - type: memory.type, - key: memory.key, - content: memory.content, - metadata: memory.metadata, - createdAt: new Date(now), - updatedAt: new Date(now), - expiresAt: memory.expiresAt ?? null, - }; - invalidateMemoryCache(id); - evictIfNeeded(_memoryCache); - _memoryCache.set(id, { value: createdMemory, timestamp: Date.now() }); - return createdMemory; -} -async function deleteMemory(id) { - if (!id || typeof id !== "string") return false; - const db2 = getDbInstance(); - const stmt = db2.prepare("DELETE FROM memory WHERE id = ?"); - const result = stmt.run(id); - if (result.changes === 0) { - return false; - } - invalidateMemoryCache(id); - return true; -} -async function listMemories(filters) { - const db2 = getDbInstance(); - let query = "SELECT * FROM memory"; - const params = []; - const whereClauses = []; - if (filters.apiKeyId) { - whereClauses.push("apiKeyId = ?"); - params.push(filters.apiKeyId); - } - if (filters.type) { - whereClauses.push("type = ?"); - params.push(filters.type); - } - if (filters.sessionId) { - whereClauses.push("sessionId = ?"); - params.push(filters.sessionId); - } - if (whereClauses.length > 0) { - query += " WHERE " + whereClauses.join(" AND "); - } - query += " ORDER BY createdAt DESC"; - if (filters.limit !== void 0) { - query += " LIMIT ?"; - params.push(filters.limit); - } - if (filters.offset !== void 0) { - query += " OFFSET ?"; - params.push(filters.offset); - } - const stmt = db2.prepare(query); - const rows = stmt.all(...params); - return rows.map((row) => ({ - id: String(row.id), - apiKeyId: String(row.apiKeyId), - sessionId: String(row.sessionId), - type: row.type, - key: String(row.key), - content: String(row.content), - metadata: parseJSON(row.metadata), - createdAt: new Date(String(row.createdAt)), - updatedAt: new Date(String(row.updatedAt)), - expiresAt: row.expiresAt ? new Date(String(row.expiresAt)) : null, - })); -} - -// open-sse/mcp-server/tools/memoryTools.ts -var MemorySearchSchema = z3.object({ - apiKeyId: z3.string(), - query: z3.string().optional(), - type: z3.enum(["factual", "episodic", "procedural", "semantic"]).optional(), - maxTokens: z3.number().int().positive().max(8e3).optional(), - limit: z3.number().int().positive().max(100).optional(), -}); -var MemoryAddSchema = z3.object({ - apiKeyId: z3.string(), - sessionId: z3.string().optional(), - type: z3.enum(["factual", "episodic", "procedural", "semantic"]), - key: z3.string().min(1), - content: z3.string().min(1), - metadata: z3.record(z3.string(), z3.unknown()).optional(), -}); -var MemoryClearSchema = z3.object({ - apiKeyId: z3.string(), - type: z3.enum(["factual", "episodic", "procedural", "semantic"]).optional(), - olderThan: z3.string().optional(), -}); -var memoryTools = { - omniroute_memory_search: { - name: "omniroute_memory_search", - description: "Search memories by query, type, or API key with token budget enforcement", - inputSchema: MemorySearchSchema, - handler: async (args) => { - const config = { - enabled: true, - maxTokens: args.maxTokens || 2e3, - retrievalStrategy: "exact", - autoSummarize: false, - persistAcrossModels: false, - retentionDays: 30, - scope: "apiKey", - }; - const memories = await retrieveMemories(args.apiKeyId, config); - const filtered = args.type ? memories.filter((m) => m.type === args.type) : memories; - const limited = args.limit ? filtered.slice(0, args.limit) : filtered; - return { - success: true, - data: { - memories: limited, - count: limited.length, - totalTokens: limited.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0), - }, - }; - }, - }, - omniroute_memory_add: { - name: "omniroute_memory_add", - description: "Add a new memory entry", - inputSchema: MemoryAddSchema, - handler: async (args) => { - const memory = await createMemory({ - apiKeyId: args.apiKeyId, - sessionId: args.sessionId || "", - type: args.type, - key: args.key, - content: args.content, - metadata: args.metadata || {}, - expiresAt: null, - }); - return { - success: true, - data: { - memory, - message: "Memory created successfully", - }, - }; - }, - }, - omniroute_memory_clear: { - name: "omniroute_memory_clear", - description: "Clear memories for an API key, optionally filtered by type or age", - inputSchema: MemoryClearSchema, - handler: async (args) => { - const memories = await listMemories({ - apiKeyId: args.apiKeyId, - type: args.type, - }); - let toDelete = memories; - if (args.olderThan) { - const cutoff = new Date(args.olderThan); - toDelete = memories.filter((m) => new Date(m.createdAt) < cutoff); - } - let deletedCount = 0; - for (const memory of toDelete) { - await deleteMemory(memory.id); - deletedCount++; - } - return { - success: true, - data: { - deletedCount, - message: `Cleared ${deletedCount} memories`, - }, - }; - }, - }, -}; - -// open-sse/mcp-server/tools/skillTools.ts -import { z as z5 } from "zod"; - -// src/lib/skills/schemas.ts -import { z as z4 } from "zod"; - -// src/lib/skills/types.ts -var SkillMode = /* @__PURE__ */ ((SkillMode2) => { - SkillMode2["AUTO"] = "auto"; - SkillMode2["MANUAL"] = "manual"; - SkillMode2["HYBRID"] = "hybrid"; - return SkillMode2; -})(SkillMode || {}); - -// src/lib/skills/schemas.ts -var SkillSchema = z4.object({ - input: z4.record(z4.string(), z4.unknown()), - output: z4.record(z4.string(), z4.unknown()), -}); -var SkillCreateInputSchema = z4 - .object({ - name: z4.string().min(1).max(100), - version: z4 - .string() - .regex(/^\d+\.\d+\.\d+$/) - .default("1.0.0"), - description: z4.string().max(500).optional(), - schema: SkillSchema, - handler: z4.string().min(1), - enabled: z4.boolean().default(true), - }) - .strict(); -var SkillUpdateInputSchema = z4 - .object({ - name: z4.string().min(1).max(100).optional(), - version: z4 - .string() - .regex(/^\d+\.\d+\.\d+$/) - .optional(), - description: z4.string().max(500).optional(), - schema: SkillSchema.optional(), - handler: z4.string().min(1).optional(), - enabled: z4.boolean().optional(), - }) - .strict(); -var SkillConfigSchema = z4.object({ - enabled: z4.boolean(), - mode: z4.nativeEnum(SkillMode), - allowedSkills: z4.array(z4.string()), - timeout: z4.number().int().positive().default(3e4), - maxRetries: z4.number().int().min(0).default(3), -}); - -// src/lib/skills/registry.ts -import { randomUUID } from "crypto"; -var SkillRegistry = class _SkillRegistry { - static instance; - registeredSkills = /* @__PURE__ */ new Map(); - versionCache = /* @__PURE__ */ new Map(); - constructor() {} - static getInstance() { - if (!_SkillRegistry.instance) { - _SkillRegistry.instance = new _SkillRegistry(); - } - return _SkillRegistry.instance; - } - async register(skillData) { - const parsed = SkillCreateInputSchema.parse(skillData); - const db2 = getDbInstance(); - const id = randomUUID(); - const now = /* @__PURE__ */ new Date(); - db2 - .prepare( - `INSERT INTO skills (id, api_key_id, name, version, description, schema, handler, enabled, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - ) - .run( - id, - skillData.apiKeyId, - parsed.name, - parsed.version, - parsed.description || null, - JSON.stringify(parsed.schema), - parsed.handler, - parsed.enabled ? 1 : 0, - now.toISOString(), - now.toISOString() - ); - const skill = { - id, - apiKeyId: skillData.apiKeyId, - name: parsed.name, - version: parsed.version, - description: parsed.description || "", - schema: parsed.schema, - handler: parsed.handler, - enabled: parsed.enabled, - createdAt: now, - updatedAt: now, - }; - this.registeredSkills.set(`${parsed.name}@${parsed.version}`, skill); - this.updateVersionCache(skill); - return skill; - } - async unregister(name, version, apiKeyId) { - const db2 = getDbInstance(); - if (version) { - const key = `${name}@${version}`; - const skill = this.registeredSkills.get(key); - if (skill && (!apiKeyId || skill.apiKeyId === apiKeyId)) { - db2.prepare("DELETE FROM skills WHERE id = ?").run(skill.id); - this.registeredSkills.delete(key); - this.clearVersionCache(name); - return true; - } - } else { - const deleted = db2 - .prepare("DELETE FROM skills WHERE name = ? AND (? IS NULL OR api_key_id = ?)") - .run(name, apiKeyId || null, apiKeyId || null); - if (deleted.changes > 0) { - const keysToDelete = Array.from(this.registeredSkills.keys()).filter((k) => - k.startsWith(`${name}@`) - ); - keysToDelete.forEach((k) => this.registeredSkills.delete(k)); - this.clearVersionCache(name); - return true; - } - } - return false; - } - list(apiKeyId) { - if (apiKeyId) { - return Array.from(this.registeredSkills.values()).filter((s) => s.apiKeyId === apiKeyId); - } - return Array.from(this.registeredSkills.values()); - } - getSkill(name, apiKeyId) { - return this.registeredSkills.get(name); - } - getSkillVersions(name) { - const cached = this.versionCache.get(name); - if (!cached) return []; - return Array.from(cached.values()).sort((a, b) => this.compareVersions(b.version, a.version)); - } - resolveVersion(name, constraint, apiKeyId) { - const versions = this.getSkillVersions(name); - if (versions.length === 0) return void 0; - const operator = constraint.charAt(0); - const version = constraint.slice(1); - switch (operator) { - case "^": - return versions.find((s) => this.satisfies(s.version, version, "^")); - case "~": - return versions.find((s) => this.satisfies(s.version, version, "~")); - case ">": - case ">=": - case "<": - case "<=": - case "==": - return versions.find((s) => this.satisfies(s.version, version, operator)); - default: - return versions.find((s) => s.version === constraint); - } - } - satisfies(version, base, operator) { - const [baseMajor, baseMinor, basePatch] = base.split(".").map(Number); - const [verMajor, verMinor, verPatch] = version.split(".").map(Number); - switch (operator) { - case "^": - return ( - verMajor === baseMajor && - (verMinor > baseMinor || (verMinor === baseMinor && verPatch >= basePatch)) - ); - case "~": - return verMajor === baseMajor && verMinor === baseMinor && verPatch >= basePatch; - case ">": - return this.compareVersions(version, base) > 0; - case ">=": - return this.compareVersions(version, base) >= 0; - case "<": - return this.compareVersions(version, base) < 0; - case "<=": - return this.compareVersions(version, base) <= 0; - case "==": - return version === base; - default: - return version === base; - } - } - compareVersions(a, b) { - const [aMajor, aMinor, aPatch] = a.split(".").map(Number); - const [bMajor, bMinor, bPatch] = b.split(".").map(Number); - if (aMajor !== bMajor) return aMajor - bMajor; - if (aMinor !== bMinor) return aMinor - bMinor; - return aPatch - bPatch; - } - updateVersionCache(skill) { - if (!this.versionCache.has(skill.name)) { - this.versionCache.set(skill.name, /* @__PURE__ */ new Map()); - } - this.versionCache.get(skill.name).set(skill.version, skill); - } - clearVersionCache(name) { - this.versionCache.delete(name); - } - async loadFromDatabase(apiKeyId) { - const db2 = getDbInstance(); - const rows = apiKeyId - ? db2.prepare("SELECT * FROM skills WHERE api_key_id = ?").all(apiKeyId) - : db2.prepare("SELECT * FROM skills").all(); - for (const row of rows) { - const skill = { - id: row.id, - apiKeyId: row.api_key_id, - name: row.name, - version: row.version, - description: row.description || "", - schema: JSON.parse(row.schema), - handler: row.handler, - enabled: row.enabled === 1, - createdAt: new Date(row.created_at), - updatedAt: new Date(row.updated_at), - }; - this.registeredSkills.set(`${skill.name}@${skill.version}`, skill); - this.updateVersionCache(skill); - } - } -}; -var skillRegistry = SkillRegistry.getInstance(); - -// src/lib/skills/executor.ts -import { randomUUID as randomUUID2 } from "crypto"; -var SkillExecutor = class _SkillExecutor { - static instance; - handlers = /* @__PURE__ */ new Map(); - timeout = 3e4; - maxRetries = 3; - constructor() {} - static getInstance() { - if (!_SkillExecutor.instance) { - _SkillExecutor.instance = new _SkillExecutor(); - } - return _SkillExecutor.instance; - } - registerHandler(name, handler) { - this.handlers.set(name, handler); - } - setTimeout(ms) { - this.timeout = ms; - } - setMaxRetries(count) { - this.maxRetries = count; - } - async execute(skillName, input, context) { - const skill = skillRegistry.getSkill(skillName, context.apiKeyId); - if (!skill) { - throw new Error(`Skill not found: ${skillName}`); - } - if (!skill.enabled) { - throw new Error(`Skill is disabled: ${skillName}`); - } - const db2 = getDbInstance(); - const executionId = randomUUID2(); - const startTime = Date.now(); - try { - db2 - .prepare( - `INSERT INTO skill_executions (id, skill_id, api_key_id, session_id, input, status, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?)` - ) - .run( - executionId, - skill.id, - context.apiKeyId, - context.sessionId || null, - JSON.stringify(input), - "running" /* RUNNING */, - /* @__PURE__ */ new Date().toISOString() - ); - const handler = this.handlers.get(skill.handler); - if (!handler) { - throw new Error(`Handler not found: ${skill.handler}`); - } - let output = null; - let errorMessage = null; - let status = "success"; /* SUCCESS */ - try { - const result = await this.executeWithTimeout( - handler(input, { apiKeyId: context.apiKeyId, sessionId: context.sessionId || "" }) - ); - output = result; - } catch (err) { - errorMessage = err instanceof Error ? err.message : String(err); - status = "error" /* ERROR */; - } - const durationMs = Date.now() - startTime; - db2 - .prepare( - `UPDATE skill_executions SET output = ?, status = ?, error_message = ?, duration_ms = ? WHERE id = ?` - ) - .run(output ? JSON.stringify(output) : null, status, errorMessage, durationMs, executionId); - return { - id: executionId, - skillId: skill.id, - apiKeyId: context.apiKeyId, - sessionId: context.sessionId || "", - input, - output, - status, - errorMessage, - durationMs, - createdAt: /* @__PURE__ */ new Date(), - }; - } catch (err) { - const durationMs = Date.now() - startTime; - const errorMessage = err instanceof Error ? err.message : String(err); - db2 - .prepare( - `UPDATE skill_executions SET status = ?, error_message = ?, duration_ms = ? WHERE id = ?` - ) - .run("error" /* ERROR */, errorMessage, durationMs, executionId); - throw err; - } - } - async executeWithTimeout(promise) { - return Promise.race([ - promise, - new Promise((_, reject) => - setTimeout(() => reject(new Error("Skill execution timed out")), this.timeout) - ), - ]); - } - getExecution(executionId) { - const db2 = getDbInstance(); - const row = db2.prepare("SELECT * FROM skill_executions WHERE id = ?").get(executionId); - if (!row) return void 0; - return { - id: row.id, - skillId: row.skill_id, - apiKeyId: row.api_key_id, - sessionId: row.session_id || "", - input: JSON.parse(row.input), - output: row.output ? JSON.parse(row.output) : null, - status: row.status, - errorMessage: row.error_message, - durationMs: row.duration_ms, - createdAt: new Date(row.created_at), - }; - } - listExecutions(apiKeyId, limit = 50) { - const db2 = getDbInstance(); - const rows = apiKeyId - ? db2 - .prepare( - "SELECT * FROM skill_executions WHERE api_key_id = ? ORDER BY created_at DESC LIMIT ?" - ) - .all(apiKeyId, limit) - : db2.prepare("SELECT * FROM skill_executions ORDER BY created_at DESC LIMIT ?").all(limit); - return rows.map((row) => ({ - id: row.id, - skillId: row.skill_id, - apiKeyId: row.api_key_id, - sessionId: row.session_id || "", - input: JSON.parse(row.input), - output: row.output ? JSON.parse(row.output) : null, - status: row.status, - errorMessage: row.error_message, - durationMs: row.duration_ms, - createdAt: new Date(row.created_at), - })); - } -}; -var skillExecutor = SkillExecutor.getInstance(); - -// open-sse/mcp-server/tools/skillTools.ts -var SkillListSchema = z5.object({ - apiKeyId: z5.string().optional(), - name: z5.string().optional(), - enabled: z5.boolean().optional(), -}); -var SkillEnableSchema = z5.object({ - apiKeyId: z5.string(), - skillId: z5.string(), - enabled: z5.boolean(), -}); -var SkillExecuteSchema = z5.object({ - apiKeyId: z5.string(), - skillName: z5.string(), - input: z5.record(z5.string(), z5.unknown()), - sessionId: z5.string().optional(), -}); -var skillTools = { - omniroute_skills_list: { - name: "omniroute_skills_list", - description: "List all registered skills with optional filtering by API key or name", - inputSchema: SkillListSchema, - handler: async (args) => { - await skillRegistry.loadFromDatabase(args.apiKeyId); - const skills = skillRegistry.list(args.apiKeyId); - let filtered = skills; - if (args.name) { - filtered = filtered.filter((s) => s.name.includes(args.name)); - } - if (args.enabled !== void 0) { - filtered = filtered.filter((s) => s.enabled === args.enabled); - } - return { - skills: filtered.map((s) => ({ - id: s.id, - name: s.name, - version: s.version, - description: s.description, - enabled: s.enabled, - createdAt: s.createdAt.toISOString(), - })), - count: filtered.length, - }; - }, - }, - omniroute_skills_enable: { - name: "omniroute_skills_enable", - description: "Enable or disable a specific skill by ID", - inputSchema: SkillEnableSchema, - handler: async (args) => { - const skill = skillRegistry.getSkill(args.skillId, args.apiKeyId); - if (!skill) { - throw new Error(`Skill not found: ${args.skillId}`); - } - await skillRegistry.register({ - ...skill, - enabled: args.enabled, - apiKeyId: args.apiKeyId, - }); - return { success: true, skillId: args.skillId, enabled: args.enabled }; - }, - }, - omniroute_skills_execute: { - name: "omniroute_skills_execute", - description: "Execute a skill with provided input and return the result", - inputSchema: SkillExecuteSchema, - handler: async (args) => { - const execution = await skillExecutor.execute(args.skillName, args.input, { - apiKeyId: args.apiKeyId, - sessionId: args.sessionId, - }); - return { - id: execution.id, - skillId: execution.skillId, - status: execution.status, - output: execution.output, - error: execution.errorMessage, - duration: execution.durationMs, - createdAt: execution.createdAt.toISOString(), - }; - }, - }, - omniroute_skills_executions: { - name: "omniroute_skills_executions", - description: "List recent skill execution history", - inputSchema: z5.object({ - apiKeyId: z5.string().optional(), - limit: z5.number().int().positive().max(100).optional(), - }), - handler: async (args) => { - const executions = skillExecutor.listExecutions(args.apiKeyId, args.limit || 50); - return { - executions: executions.map((e) => ({ - id: e.id, - skillId: e.skillId, - status: e.status, - duration: e.durationMs, - error: e.errorMessage, - createdAt: e.createdAt.toISOString(), - })), - count: executions.length, - }; - }, - }, -}; - -// open-sse/mcp-server/server.ts -var OMNIROUTE_BASE_URL2 = process.env.OMNIROUTE_BASE_URL || "http://localhost:20128"; -var OMNIROUTE_API_KEY2 = process.env.OMNIROUTE_API_KEY || ""; -var MCP_ENFORCE_SCOPES = process.env.OMNIROUTE_MCP_ENFORCE_SCOPES === "true"; -var MCP_ALLOWED_SCOPES = new Set( - (process.env.OMNIROUTE_MCP_SCOPES || "") - .split(",") - .map((s) => s.trim()) - .filter(Boolean) -); -function toRecord2(value) { - return value && typeof value === "object" && !Array.isArray(value) ? value : {}; -} -function toArray(value) { - return Array.isArray(value) ? value : []; -} -function toString2(value, fallback = "") { - return typeof value === "string" ? value : fallback; -} -function toNumber3(value, fallback = 0) { - return typeof value === "number" && Number.isFinite(value) ? value : fallback; -} -function toStringArray(value, fallback = []) { - const values = toArray(value).filter((entry) => typeof entry === "string"); - return values.length > 0 ? values : fallback; -} -function normalizeComboModels(rawModels) { - return toArray(rawModels).map((rawModel, index) => { - const model = toRecord2(rawModel); - return { - provider: toString2(model.provider, "unknown"), - model: toString2(model.model, "unknown"), - priority: toNumber3(model.priority, index + 1), - }; - }); -} -async function omniRouteFetch(path4, options = {}) { - const url = `${OMNIROUTE_BASE_URL2}${path4}`; - const headers = { - "Content-Type": "application/json", - ...(OMNIROUTE_API_KEY2 ? { Authorization: `Bearer ${OMNIROUTE_API_KEY2}` } : {}), - ...(options.headers || {}), - }; - const response = await fetch(url, { ...options, headers, signal: AbortSignal.timeout(1e4) }); - if (!response.ok) { - const errorText = await response.text().catch(() => "Unknown error"); - throw new Error(`OmniRoute API error [${response.status}]: ${errorText}`); - } - return response.json(); -} -function withScopeEnforcement(toolName, handler) { - return async (args, extra) => { - const scopeContext = resolveCallerScopeContext(extra, Array.from(MCP_ALLOWED_SCOPES)); - const scopeCheck = evaluateToolScopes(toolName, scopeContext.scopes, MCP_ENFORCE_SCOPES); - if (!scopeCheck.allowed) { - const missingScopes = - scopeCheck.missing.length > 0 ? scopeCheck.missing.join(", ") : "unavailable"; - const reason = scopeCheck.reason || "scope_check_failed"; - const msg = `Insufficient MCP scopes for ${toolName}. Missing: ${missingScopes}. Caller=${scopeContext.callerId}, source=${scopeContext.source}.`; - const safeArgs = args && typeof args === "object" ? toRecord2(args) : { rawArgs: args }; - await logToolCall( - toolName, - { - ...safeArgs, - _scopeCheck: { - callerId: scopeContext.callerId, - source: scopeContext.source, - required: scopeCheck.required, - provided: scopeCheck.provided, - missing: scopeCheck.missing, - }, - }, - null, - 0, - false, - `scope_denied:${reason}` - ); - return { - content: [{ type: "text", text: `Error: ${msg}` }], - isError: true, - }; - } - return handler(args, extra); - }; -} -async function handleGetHealth() { - const start = Date.now(); - try { - const [healthRaw, resilienceRaw, rateLimitsRaw] = await Promise.allSettled([ - omniRouteFetch("/api/monitoring/health"), - omniRouteFetch("/api/resilience"), - omniRouteFetch("/api/rate-limits"), - ]); - const health = healthRaw.status === "fulfilled" ? toRecord2(healthRaw.value) : {}; - const resilience = resilienceRaw.status === "fulfilled" ? toRecord2(resilienceRaw.value) : {}; - const rateLimits = rateLimitsRaw.status === "fulfilled" ? toRecord2(rateLimitsRaw.value) : {}; - const memoryUsageRaw = toRecord2(health.memoryUsage); - const cacheStatsRaw = toRecord2(health.cacheStats); - const resilienceCircuitBreakers = toArray(resilience.circuitBreakers); - const rateLimitEntries = toArray(rateLimits.limits); - const result = { - uptime: toString2(health.uptime, "unknown"), - version: toString2(health.version, "unknown"), - memoryUsage: { - heapUsed: toNumber3(memoryUsageRaw.heapUsed, 0), - heapTotal: toNumber3(memoryUsageRaw.heapTotal, 0), - }, - circuitBreakers: resilienceCircuitBreakers, - rateLimits: rateLimitEntries, - cacheStats: - Object.keys(cacheStatsRaw).length > 0 - ? { - hits: toNumber3(cacheStatsRaw.hits, 0), - misses: toNumber3(cacheStatsRaw.misses, 0), - hitRate: toNumber3(cacheStatsRaw.hitRate, 0), - } - : void 0, - }; - await logToolCall("omniroute_get_health", {}, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_get_health", {}, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleListCombos(args) { - const start = Date.now(); - try { - const combosRaw = await omniRouteFetch("/api/combos"); - const combosRecord = toRecord2(combosRaw); - const combos = Array.isArray(combosRecord.combos) - ? combosRecord.combos - : Array.isArray(combosRaw) - ? combosRaw - : []; - let metrics = {}; - if (args.includeMetrics) { - metrics = toRecord2(await omniRouteFetch("/api/combos/metrics").catch(() => ({}))); - } - const result = { - combos: toArray(combos).map((rawCombo) => { - const combo = toRecord2(rawCombo); - const comboData = toRecord2(combo.data); - const comboId = toString2(combo.id, ""); - const modelsSource = - Array.isArray(combo.models) && combo.models.length > 0 ? combo.models : comboData.models; - return { - id: comboId, - name: toString2(combo.name, comboId || "unnamed"), - models: normalizeComboModels(modelsSource), - strategy: toString2(combo.strategy, toString2(comboData.strategy, "priority")), - enabled: combo.enabled !== false, - ...(args.includeMetrics ? { metrics: metrics[comboId] ?? null } : {}), - }; - }), - }; - await logToolCall("omniroute_list_combos", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_list_combos", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleGetComboMetrics(args) { - const start = Date.now(); - try { - const result = await omniRouteFetch( - `/api/combos/metrics?comboId=${encodeURIComponent(args.comboId)}` - ); - await logToolCall("omniroute_get_combo_metrics", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_get_combo_metrics", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleSwitchCombo(args) { - const start = Date.now(); - try { - const result = await omniRouteFetch(`/api/combos/${encodeURIComponent(args.comboId)}`, { - method: "PUT", - body: JSON.stringify({ isActive: args.active }), - }); - await logToolCall("omniroute_switch_combo", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_switch_combo", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleCheckQuota(args) { - const start = Date.now(); - try { - let path4 = "/api/usage/quota"; - if (args.connectionId) path4 += `?connectionId=${encodeURIComponent(args.connectionId)}`; - else if (args.provider) path4 += `?provider=${encodeURIComponent(args.provider)}`; - const result = normalizeQuotaResponse(await omniRouteFetch(path4), { - provider: args.provider || null, - connectionId: args.connectionId || null, - }); - await logToolCall("omniroute_check_quota", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_check_quota", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleRouteRequest(args) { - const start = Date.now(); - try { - const body = { - model: args.model, - messages: args.messages, - stream: false, - // MCP tool always returns non-streaming - }; - if (args.combo) { - body["x-combo"] = args.combo; - } - const raw = await omniRouteFetch("/v1/chat/completions", { - method: "POST", - body: JSON.stringify(body), - }); - const choices = toArray(raw.choices); - const firstChoice = toRecord2(choices[0]); - const firstMessage = toRecord2(firstChoice.message); - const usage = toRecord2(raw.usage); - const result = { - response: { - content: toString2(firstMessage.content, ""), - model: toString2(raw.model, args.model), - tokens: { - prompt: toNumber3(usage.prompt_tokens, 0), - completion: toNumber3(usage.completion_tokens, 0), - }, - }, - routing: { - provider: toString2(raw.provider, "unknown"), - combo: raw.combo ?? null, - fallbacksTriggered: toNumber3(raw.fallbacksTriggered, 0), - cost: toNumber3(raw.cost, 0), - latencyMs: Date.now() - start, - routingExplanation: toString2( - raw.routingExplanation, - "Request routed through primary provider" - ), - }, - }; - await logToolCall( - "omniroute_route_request", - { model: args.model, messageCount: args.messages.length }, - result.routing, - Date.now() - start, - true - ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall( - "omniroute_route_request", - { model: args.model }, - null, - Date.now() - start, - false, - msg - ); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleCostReport(args) { - const start = Date.now(); - try { - const period = args.period || "session"; - const rangeMap = { - session: "1d", - day: "1d", - week: "7d", - month: "30d", - }; - const range = rangeMap[period] || "30d"; - const raw = toRecord2( - await omniRouteFetch(`/api/usage/analytics?range=${encodeURIComponent(range)}`) - ); - const tokenCount = toRecord2(raw.tokenCount); - const budget = toRecord2(raw.budget); - const result = { - period, - totalCost: toNumber3(raw.totalCost, 0), - requestCount: toNumber3(raw.requestCount, 0), - tokenCount: { - prompt: toNumber3(tokenCount.prompt, 0), - completion: toNumber3(tokenCount.completion, 0), - }, - byProvider: toArray(raw.byProvider), - byModel: toArray(raw.byModel), - budget: { - limit: budget.limit ?? null, - remaining: budget.remaining ?? null, - }, - }; - await logToolCall("omniroute_cost_report", args, result, Date.now() - start, true); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_cost_report", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -async function handleListModelsCatalog(args) { - const start = Date.now(); - try { - let path4 = "/v1/models"; - let isProviderSpecific = false; - let source = "local_catalog"; - let warning; - if (args.provider && !args.capability) { - path4 = `/api/providers/${encodeURIComponent(args.provider)}/models?excludeHidden=true`; - isProviderSpecific = true; - } else { - const params = new URLSearchParams(); - if (args.provider) params.set("provider", args.provider); - if (args.capability) params.set("capability", args.capability); - if (params.toString()) path4 += `?${params.toString()}`; - } - const raw = toRecord2(await omniRouteFetch(path4)); - let rawModels = []; - if (isProviderSpecific) { - rawModels = Array.isArray(raw.models) ? raw.models : []; - source = typeof raw.source === "string" ? raw.source : "api"; - if (raw.warning) warning = String(raw.warning); - } else { - rawModels = Array.isArray(raw.data) ? raw.data : []; - source = "local_catalog"; - } - const result = { - models: rawModels.map((rawModel) => { - const model = toRecord2(rawModel); - return { - id: toString2(model.id, ""), - provider: toString2( - model.owned_by, - toString2(model.provider, args.provider || "unknown") - ), - capabilities: toStringArray(model.capabilities, ["chat"]), - status: toString2(model.status, "available"), - pricing: model.pricing, - }; - }), - source, - ...(warning ? { warning } : {}), - }; - await logToolCall( - "omniroute_list_models_catalog", - args, - { modelCount: result.models.length }, - Date.now() - start, - true - ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - await logToolCall("omniroute_list_models_catalog", args, null, Date.now() - start, false, msg); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } -} -function createMcpServer() { - const server = new McpServer({ - name: "omniroute", - version: process.env.npm_package_version || "1.8.1", - }); - server.registerTool( - "omniroute_get_health", - { - description: - "Returns OmniRoute health status including uptime, memory, circuit breakers, rate limits, and cache stats", - inputSchema: getHealthInput, - }, - withScopeEnforcement("omniroute_get_health", async (args) => { - getHealthInput.parse(args ?? {}); - return handleGetHealth(); - }) - ); - server.registerTool( - "omniroute_list_combos", - { - description: - "Lists all configured combos (model chains) with strategies and optional metrics", - inputSchema: listCombosInput, - }, - withScopeEnforcement("omniroute_list_combos", (args) => - handleListCombos(listCombosInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_get_combo_metrics", - { - description: "Returns detailed performance metrics for a specific combo", - inputSchema: getComboMetricsInput, - }, - withScopeEnforcement("omniroute_get_combo_metrics", (args) => - handleGetComboMetrics(getComboMetricsInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_switch_combo", - { - description: "Activates or deactivates a combo for routing", - inputSchema: switchComboInput, - }, - withScopeEnforcement("omniroute_switch_combo", (args) => - handleSwitchCombo(switchComboInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_check_quota", - { - description: "Checks remaining API quota for one or all providers", - inputSchema: checkQuotaInput, - }, - withScopeEnforcement("omniroute_check_quota", (args) => - handleCheckQuota(checkQuotaInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_route_request", - { - description: "Sends a chat completion request through OmniRoute intelligent routing", - inputSchema: routeRequestInput, - }, - withScopeEnforcement("omniroute_route_request", (args) => - handleRouteRequest(routeRequestInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_cost_report", - { - description: "Generates a cost report for the specified period", - inputSchema: costReportInput, - }, - withScopeEnforcement("omniroute_cost_report", (args) => - handleCostReport(costReportInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_list_models_catalog", - { - description: "Lists all available AI models across providers with capabilities and pricing", - inputSchema: listModelsCatalogInput, - }, - withScopeEnforcement("omniroute_list_models_catalog", (args) => - handleListModelsCatalog(listModelsCatalogInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_simulate_route", - { - description: "Simulates the routing path a request would take without executing it (dry-run)", - inputSchema: simulateRouteInput, - }, - withScopeEnforcement("omniroute_simulate_route", (args) => - handleSimulateRoute(simulateRouteInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_set_budget_guard", - { - description: - "Sets a session budget limit with configurable action when exceeded (degrade/block/alert)", - inputSchema: setBudgetGuardInput, - }, - withScopeEnforcement("omniroute_set_budget_guard", (args) => - handleSetBudgetGuard(setBudgetGuardInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_set_routing_strategy", - { - description: - "Updates combo routing strategy at runtime (priority/weighted/round-robin/auto/etc.)", - inputSchema: setRoutingStrategyInput, - }, - withScopeEnforcement("omniroute_set_routing_strategy", (args) => - handleSetRoutingStrategy(setRoutingStrategyInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_set_resilience_profile", - { - description: - "Applies a resilience profile controlling circuit breakers, retries, timeouts, and fallback depth", - inputSchema: setResilienceProfileInput, - }, - withScopeEnforcement("omniroute_set_resilience_profile", (args) => - handleSetResilienceProfile(setResilienceProfileInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_test_combo", - { - description: - "Tests each provider in a combo with a real prompt, reporting latency, cost, and success per provider", - inputSchema: testComboInput, - }, - withScopeEnforcement("omniroute_test_combo", (args) => - handleTestCombo(testComboInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_get_provider_metrics", - { - description: - "Returns detailed metrics for a specific provider including latency percentiles and circuit breaker state", - inputSchema: getProviderMetricsInput, - }, - withScopeEnforcement("omniroute_get_provider_metrics", (args) => - handleGetProviderMetrics(getProviderMetricsInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_best_combo_for_task", - { - description: - "Recommends the best combo for a task type based on provider fitness and constraints", - inputSchema: bestComboForTaskInput, - }, - withScopeEnforcement("omniroute_best_combo_for_task", (args) => - handleBestComboForTask(bestComboForTaskInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_explain_route", - { - description: - "Explains why a request was routed to a specific provider, showing scoring factors and fallbacks", - inputSchema: explainRouteInput, - }, - withScopeEnforcement("omniroute_explain_route", (args) => - handleExplainRoute(explainRouteInput.parse(args)) - ) - ); - server.registerTool( - "omniroute_get_session_snapshot", - { - description: - "Returns a full snapshot of the current working session: cost, tokens, top models, errors, budget status", - inputSchema: getSessionSnapshotInput, - }, - withScopeEnforcement("omniroute_get_session_snapshot", async (args) => { - getSessionSnapshotInput.parse(args ?? {}); - return handleGetSessionSnapshot(); - }) - ); - server.registerTool( - "omniroute_sync_pricing", - { - description: - "Syncs pricing data from external sources (LiteLLM) into OmniRoute without overwriting user-set prices", - inputSchema: syncPricingInput, - }, - withScopeEnforcement("omniroute_sync_pricing", (args) => - handleSyncPricing(syncPricingInput.parse(args)) - ) - ); - Object.values(memoryTools).forEach((toolDef) => { - server.registerTool( - toolDef.name, - { - description: toolDef.description, - // @ts-ignore: dynamic zod access - inputSchema: toolDef.inputSchema, - }, - withScopeEnforcement(toolDef.name, async (args) => { - try { - const parsedArgs = toolDef.inputSchema.parse(args ?? {}); - const result = await toolDef.handler(parsedArgs); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } - }) - ); - }); - Object.values(skillTools).forEach((toolDef) => { - server.registerTool( - toolDef.name, - { - description: toolDef.description, - // @ts-ignore: dynamic zod access - inputSchema: toolDef.inputSchema, - }, - withScopeEnforcement(toolDef.name, async (args) => { - try { - const parsedArgs = toolDef.inputSchema.parse(args ?? {}); - const result = await toolDef.handler(parsedArgs); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true }; - } - }) - ); - }); - return server; -} -async function startMcpStdio() { - const server = createMcpServer(); - const transport = new StdioServerTransport(); - const version = process.env.npm_package_version || "1.8.1"; - const stopHeartbeat = startMcpHeartbeat({ - version, - scopesEnforced: MCP_ENFORCE_SCOPES, - allowedScopes: Array.from(MCP_ALLOWED_SCOPES), - toolCount: MCP_TOOLS.length, - }); - const stopHeartbeatOnce = () => { - stopHeartbeat(); - }; - process.once("exit", stopHeartbeatOnce); - process.once("SIGINT", stopHeartbeatOnce); - process.once("SIGTERM", stopHeartbeatOnce); - console.error("[MCP] OmniRoute MCP Server starting (stdio transport)..."); - try { - await server.connect(transport); - console.error("[MCP] OmniRoute MCP Server connected and ready."); - } finally { - stopHeartbeatOnce(); - process.off("exit", stopHeartbeatOnce); - process.off("SIGINT", stopHeartbeatOnce); - process.off("SIGTERM", stopHeartbeatOnce); - } -} -if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"))) { - startMcpStdio().catch((err) => { - console.error("[MCP] Fatal error:", err); - process.exit(1); - }); -} -export { createMcpServer, startMcpStdio }; diff --git a/test-port.js b/test-port.js deleted file mode 100644 index f64341a044..0000000000 --- a/test-port.js +++ /dev/null @@ -1,28 +0,0 @@ -function extractExplicitPort(urlStr) { - try { - const idx = urlStr.indexOf("://"); - if (idx === -1) return null; - const authorityStart = idx + 3; - const authorityEnd = urlStr.indexOf("/", authorityStart); - const authority = - authorityEnd === -1 - ? urlStr.slice(authorityStart) - : urlStr.slice(authorityStart, authorityEnd); - const lastColon = authority.lastIndexOf(":"); - const atSign = authority.lastIndexOf("@"); - // The colon must be AFTER the @ sign (if any) to be the port colon - if (lastColon !== -1 && lastColon > atSign) { - const portStr = authority.slice(lastColon + 1); - if (/^\d+$/.test(portStr)) { - const port = Number(portStr); - if (Number.isInteger(port) && port >= 1 && port <= 65535) return String(port); - } - } - } catch {} - return null; -} - -console.log(extractExplicitPort("http://localhost:80")); -console.log(extractExplicitPort("http://u:p@localhost:80/path?q=1")); -console.log(extractExplicitPort("http://u:p@localhost/path:80")); -console.log(extractExplicitPort("http://u:p@localhost")); diff --git a/test_translator.mjs b/test_translator.mjs deleted file mode 100644 index e6f1d7fc4f..0000000000 --- a/test_translator.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { translateNonStreamingResponse } from "./open-sse/handlers/responseTranslator.ts"; -import { FORMATS } from "./open-sse/translator/formats.ts"; -console.log( - translateNonStreamingResponse( - { object: "chat.completion", choices: [] }, - FORMATS.CLAUDE, - FORMATS.OPENAI - ) -); diff --git a/tests/unit/cache-control-claude-providers.test.mjs b/tests/unit/cache-control-claude-providers.test.mjs index 820a1676cd..a2a933e283 100644 --- a/tests/unit/cache-control-claude-providers.test.mjs +++ b/tests/unit/cache-control-claude-providers.test.mjs @@ -139,7 +139,7 @@ describe("Cache Control Policy - Claude Protocol Providers", () => { ); }); - test("shouldPreserveCacheControl defaults CC-compatible providers to OmniRoute-managed cache in auto mode", () => { + test("shouldPreserveCacheControl treats CC-compatible providers like other Claude providers in auto mode", () => { const claudeCodeUA = "Claude-Code/1.0.0"; assert.equal( @@ -150,7 +150,7 @@ describe("Cache Control Policy - Claude Protocol Providers", () => { targetFormat: "claude", settings: { alwaysPreserveClientCache: "auto" }, }), - false + true ); }); }); diff --git a/tests/unit/cc-compatible-provider.test.mjs b/tests/unit/cc-compatible-provider.test.mjs index 961905a648..f0d00a6163 100644 --- a/tests/unit/cc-compatible-provider.test.mjs +++ b/tests/unit/cc-compatible-provider.test.mjs @@ -104,17 +104,16 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t { role: "user", text: "u2" }, ] ); - assert.deepEqual(payload.messages[0].content.at(-1).cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.messages[1].content.at(-1).cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.messages[2].content.at(-1).cache_control, { type: "ephemeral" }); + assert.equal(payload.messages[0].content.at(-1).cache_control, undefined); + assert.equal(payload.messages[1].content.at(-1).cache_control, undefined); + assert.equal(payload.messages[2].content.at(-1).cache_control, undefined); assert.equal(payload.system.length, 4); assert.equal(payload.system.at(-1).text, "sys"); - assert.deepEqual(payload.system[1].cache_control, { type: "ephemeral", ttl: "1h" }); - assert.deepEqual(payload.system[2].cache_control, { type: "ephemeral", ttl: "1h" }); - assert.deepEqual(payload.system[3].cache_control, { type: "ephemeral", ttl: "1h" }); + assert.equal(payload.system[1].cache_control, undefined); + assert.equal(payload.system[2].cache_control, undefined); + assert.equal(payload.system[3].cache_control, undefined); assert.equal(payload.tools.length, 1); - const { cache_control, ...toolWithoutCacheControl } = payload.tools[0]; - assert.deepEqual(toolWithoutCacheControl, { + assert.deepEqual(payload.tools[0], { name: "lookup_weather", description: "Fetch weather", input_schema: { @@ -125,7 +124,6 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t required: ["city"], }, }); - assert.deepEqual(payload.tools[0].cache_control, { type: "ephemeral", ttl: "1h" }); assert.deepEqual(payload.tool_choice, { type: "any" }); assert.equal(payload.context_management.edits[0].type, "clear_thinking_20251015"); assert.equal(JSON.parse(payload.metadata.user_id).session_id, "session-1"); @@ -185,11 +183,11 @@ test("buildClaudeCodeCompatibleRequest preserves Claude cache markers when reque type: "ephemeral", ttl: "10m", }); - assert.deepEqual(payload.messages[2].content[0].cache_control, { type: "ephemeral" }); + assert.equal(payload.messages[2].content[0].cache_control, undefined); assert.deepEqual(payload.tools[0].cache_control, { type: "ephemeral", ttl: "30m" }); }); -test("buildClaudeCodeCompatibleRequest supplements missing Claude cache markers in preserve mode", () => { +test("buildClaudeCodeCompatibleRequest does not supplement missing Claude cache markers in preserve mode", () => { const payload = buildClaudeCodeCompatibleRequest({ sourceBody: { max_tokens: 64, @@ -230,14 +228,14 @@ test("buildClaudeCodeCompatibleRequest supplements missing Claude cache markers preserveCacheControl: true, }); - assert.deepEqual(payload.messages[0].content[0].cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.messages[1].content[0].cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.messages[2].content[0].cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.system.at(-1).cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.tools[0].cache_control, { type: "ephemeral", ttl: "1h" }); + assert.equal(payload.messages[0].content[0].cache_control, undefined); + assert.equal(payload.messages[1].content[0].cache_control, undefined); + assert.equal(payload.messages[2].content[0].cache_control, undefined); + assert.equal(payload.system.at(-1).cache_control, undefined); + assert.equal(payload.tools[0].cache_control, undefined); }); -test("buildClaudeCodeCompatibleRequest upgrades built-in system cache markers when preserved system uses 1h", () => { +test("buildClaudeCodeCompatibleRequest keeps built-in system blocks untagged when preserved system uses 1h", () => { const payload = buildClaudeCodeCompatibleRequest({ sourceBody: { max_tokens: 64, @@ -259,13 +257,13 @@ test("buildClaudeCodeCompatibleRequest upgrades built-in system cache markers wh preserveCacheControl: true, }); - assert.deepEqual(payload.system[1].cache_control, { type: "ephemeral", ttl: "1h" }); - assert.deepEqual(payload.system[2].cache_control, { type: "ephemeral", ttl: "1h" }); - assert.deepEqual(payload.system[3].cache_control, { type: "ephemeral", ttl: "1h" }); + assert.equal(payload.system[1].cache_control, undefined); + assert.equal(payload.system[2].cache_control, undefined); + assert.deepEqual(payload.system[3].cache_control, { type: "ephemeral" }); assert.deepEqual(payload.system[4].cache_control, { type: "ephemeral", ttl: "1h" }); }); -test("buildClaudeCodeCompatibleRequest marks final user turn and 1h system cache in non-preserve mode", () => { +test("buildClaudeCodeCompatibleRequest does not add cache markers in non-preserve mode", () => { const largeUserPrompt = Array.from( { length: 200 }, (_, index) => `Context line ${index + 1}: repeated stable context for cache testing.` @@ -289,12 +287,12 @@ test("buildClaudeCodeCompatibleRequest marks final user turn and 1h system cache preserveCacheControl: false, }); - assert.deepEqual(payload.system[1].cache_control, { type: "ephemeral", ttl: "1h" }); - assert.deepEqual(payload.system[2].cache_control, { type: "ephemeral", ttl: "1h" }); - assert.deepEqual(payload.system[3].cache_control, { type: "ephemeral", ttl: "1h" }); - assert.deepEqual(payload.messages[0].content[0].cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.messages[1].content[0].cache_control, { type: "ephemeral" }); - assert.deepEqual(payload.messages[2].content[0].cache_control, { type: "ephemeral" }); + assert.equal(payload.system[1].cache_control, undefined); + assert.equal(payload.system[2].cache_control, undefined); + assert.equal(payload.system[3].cache_control, undefined); + assert.equal(payload.messages[0].content[0].cache_control, undefined); + assert.equal(payload.messages[1].content[0].cache_control, undefined); + assert.equal(payload.messages[2].content[0].cache_control, undefined); }); test("buildClaudeCodeCompatibleRequest falls back to a user turn when the source only has assistant/model text", () => { @@ -312,7 +310,7 @@ test("buildClaudeCodeCompatibleRequest falls back to a user turn when the source assert.deepEqual(payload.messages, [ { role: "user", - content: [{ type: "text", text: "draft", cache_control: { type: "ephemeral" } }], + content: [{ type: "text", text: "draft" }], }, ]); }); @@ -354,7 +352,7 @@ test("buildClaudeCodeCompatibleRequest omits auto tool_choice while preserving t }); assert.equal(payload.tools.length, 1); - assert.deepEqual(payload.tools[0].cache_control, { type: "ephemeral", ttl: "1h" }); + assert.equal(payload.tools[0].cache_control, undefined); assert.equal(payload.tool_choice, undefined); }); @@ -522,6 +520,16 @@ test("handleChatCore forces upstream streaming for CC compatible while returning assert.equal(calls.length, 1); assert.equal(calls[0].headers.Accept, "text/event-stream"); assert.equal(calls[0].body.stream, true); + assert.equal( + calls[0].body.system.some((block) => block.cache_control !== undefined), + false + ); + assert.equal( + calls[0].body.messages.some((message) => + message.content.some((block) => block.cache_control !== undefined) + ), + false + ); const payload = await result.response.json(); assert.equal(payload.choices[0].message.content, "Hello from CC"); @@ -530,7 +538,7 @@ test("handleChatCore forces upstream streaming for CC compatible while returning assert.equal(payload.usage.completion_tokens, 5); }); -test("handleChatCore applies OmniRoute-managed cache strategy for CC-compatible requests in auto mode", async () => { +test("handleChatCore preserves client cache markers for Claude Code requests to CC-compatible providers", async () => { const calls = []; globalThis.fetch = async (url, init = {}) => { calls.push({ @@ -635,20 +643,19 @@ test("handleChatCore applies OmniRoute-managed cache strategy for CC-compatible assert.equal(calls.length, 1); assert.deepEqual(calls[0].body.system.at(-1).cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "5m", }); assert.deepEqual(calls[0].body.messages[0].content[0].cache_control, { type: "ephemeral", }); assert.deepEqual(calls[0].body.messages[1].content[0].cache_control, { type: "ephemeral", + ttl: "10m", }); - assert.deepEqual(calls[0].body.messages[2].content[0].cache_control, { - type: "ephemeral", - }); + assert.equal(calls[0].body.messages[2].content[0].cache_control, undefined); assert.deepEqual(calls[0].body.tools[0].cache_control, { type: "ephemeral", - ttl: "1h", + ttl: "30m", }); }); diff --git a/tests/unit/claude-code-rendering-fixes.test.mjs b/tests/unit/claude-code-rendering-fixes.test.mjs index 09cce6d217..bb8012631a 100644 --- a/tests/unit/claude-code-rendering-fixes.test.mjs +++ b/tests/unit/claude-code-rendering-fixes.test.mjs @@ -150,6 +150,72 @@ test("Responses->Chat: empty-name tool call is dropped when done still has no va assert.equal(state.toolCallIndex, 0); }); +test("Claude->Responses: {event,data} items bypass sanitization in translate mode", async () => { + // Regression test: when translating Claude-format (GLM) to Responses API for Codex CLI, + // the sanitizer was stripping {event,data} items to {"object":"chat.completion.chunk"}, + // losing all content and the critical response.completed event. + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + // Create stream translating claude → openai-responses (same path as GLM via Codex CLI) + const stream = createSSETransformStreamWithLogger( + FORMATS.CLAUDE, + FORMATS.OPENAI_RESPONSES, + "glm", + null, + null, + "glm-5.1", + "conn-test", + { messages: [{ role: "user", content: "hi" }] }, + null, + null + ); + + const writer = stream.writable.getWriter(); + // Simulate Claude-format SSE from GLM + await writer.write( + encoder.encode( + 'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_test","type":"message","role":"assistant","model":"glm-5.1","content":[],"stop_reason":null,"usage":{"input_tokens":10,"output_tokens":0}}}\n\n' + ) + ); + await writer.write( + encoder.encode( + 'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n' + ) + ); + await writer.write( + encoder.encode( + 'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hello"}}\n\n' + ) + ); + await writer.write( + encoder.encode( + 'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}\n\n' + ) + ); + await writer.write(encoder.encode('event: message_stop\ndata: {"type":"message_stop"}\n\n')); + await writer.close(); + + const reader = stream.readable.getReader(); + let output = ""; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + output += decoder.decode(value, { stream: true }); + } + output += decoder.decode(); + + // Must emit Responses API events (not sanitized chat.completion.chunk objects) + assert.match(output, /event: response\.created/); + assert.match(output, /event: response\.output_text\.delta/); + assert.match(output, /event: response\.completed/); + assert.match(output, /"delta":"hello"/); + assert.match(output, /"status":"completed"/); + + // Must NOT contain sanitized empty chunks + assert.doesNotMatch(output, /data: \{"object":"chat\.completion\.chunk"\}\n\n/); +}); + test("Responses->Claude: translated Claude SSE is not sanitized into empty OpenAI chunks", async () => { const encoder = new TextEncoder(); const decoder = new TextDecoder(); diff --git a/tests/unit/context-manager.test.mjs b/tests/unit/context-manager.test.mjs index f4d5d5f292..1ce7125681 100644 --- a/tests/unit/context-manager.test.mjs +++ b/tests/unit/context-manager.test.mjs @@ -1,11 +1,8 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { - compressContext, - estimateTokens, - getTokenLimit, -} = await import("../../open-sse/services/contextManager.ts"); +const { compressContext, estimateTokens, getTokenLimit } = + await import("../../open-sse/services/contextManager.ts"); // ─── estimateTokens ───────────────────────────────────────────────────────── @@ -26,7 +23,7 @@ test("getTokenLimit: detects claude", () => { }); test("getTokenLimit: detects gemini", () => { - assert.equal(getTokenLimit("gemini", "gemini-2.5-pro"), 1000000); + assert.equal(getTokenLimit("gemini", "gemini-2.5-pro"), 1048576); }); test("getTokenLimit: default fallback", () => {