✨ Added
- MiMoCode free-tier provider ([#3659] — thanks @pizzav-xyz): new no-auth provider
mimocode(aliasmcode) exposing Xiaomi'smimo-automodel (1M context) via device-fingerprint bootstrap-JWT auth (/api/free-ai/bootstrap→ Bearer JWT →/api/free-ai/openai/chat). Supports multiple accounts (N fingerprints → round-robin with exponential cooldown), re-bootstrap on 401/403, and cooldown on 429. Reuses a new genericNoAuthAccountCarddashboard component (also wired foropencode). 22 unit tests; upstream validated live during review. (Maintainer follow-up: added the requiredauthHeader: "none"field to the registry entry.) Co-authored with @pizzav-xyz. - Prefer Claude Code for unprefixed
claude-*model IDs ([#3540] — thanks @Witroch4): opt-in setting (default off) that routes bareclaude-*model IDs from Claude Code clients through the Claude Code OAuth account instead of requiring a provider prefix. Configurable via theOMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELSenv flag or a dashboard toggle on the Claude provider page; explicit provider prefixes still win. Full layer coverage (resolver + DB setting + zod schemas + types + UI) with 6 tests. Co-authored with @Witroch4. - Codex Responses-WebSocket call history ([#3616] — thanks @kkkayye): Codex
/v1/responsesWebSocket calls are now persisted to request history — success completions plus prepare-failures, upstream WS errors and premature closes — withsanitizeErrorMessageapplied to the stored error. Two proxy-side integration tests cover the success and failure paths. - Obsidian/WebDAV: add the
/api/v1/webdavfile server (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE, Basic-Auth, path-traversal hardened) so Obsidian mobile can sync the vault (#3485, part 2). Implemented in the custom server layer (scripts/dev/webdav-handler.mjs) — intercepted before Next.js to support non-standard HTTP methods (PROPFIND,MKCOL,MOVE,LOCK). Reads vault path and credentials (with enc:v1: AES-256-GCM decryption) directly from the SQLitekey_valuetable; credentials configured via PR1's/api/settings/obsidian/webdavendpoint. 36 TDD unit tests covering traversal guard, constant-time auth, decrypt round-trip, XML generation, and full CRUD cycle. - Quota overview: deactivate/activate an account directly from the quota card header (toggle button) so users can park a near-zero-quota account without navigating to the provider detail page. (#3675 — thanks @leninejunior)
♻️ Code Quality
-
providers/[id]: extract
useProviderConnections,useProviderSettings,useProviderModelshooks from the god-component — #3501 Phase 1f.ProviderDetailPageClient.tsx: 4,948 → 4,063 LOC (−885 lines). New hooks inhooks/:useProviderConnections.ts(954 LOC — all connection management, batch ops, proxy/CLIProxyAPI state, batch-test runner with MAX_BULK_IDS chunking),useProviderSettings.ts(264 LOC — Codex global service mode + Claude routing preference),useProviderModels.ts(155 LOC — model metadata, aliases). Frozen baselines updated. 10 Phase-1f smoke tests; typecheck/cycles/lint green. Co-authored with @oyi77. -
providers/[id]: extract
useModelCompatStatehook + model sections (ModelRow,PassthroughModelRow,PassthroughModelsSection,CustomModelsSection,CompatibleModelsSection) from the god-component — #3501 Phase 1e.ProviderDetailPageClient.tsx: 6,838 → 4,922 LOC (−1,916 lines). New leafhooks/useModelCompatState.ts(101 LOC); compat helpers moved toproviderPageHelpers.ts. Frozen baselines:providerPageHelpers.ts: 822. 12 Phase-1e smoke tests; typecheck/cycles/lint green; #3610 auto-hide fix preserved. -
providers/[id]: extract
ConnectionRow(+CooldownTimer/inferErrorType/getStatusPresentation),ModelCompatPopover(+recordToHeaderRows), andSiliconFlowEndpointModalfrom the god-component intocomponents/— #3501 Phase 1d.ProviderDetailPageClient.tsx: 8,092 → 6,838 LOC (−1,254 lines). Frozen baselines:ConnectionRow.tsx: 941. 7 new Phase-1d smoke tests; typecheck/cycles/lint green. -
providers/[id]: extract AddApiKeyModal + EditConnectionModal (+ WebSessionCredentialGuide) from the god-component into components/ ([#3501] Phase 1c): extracted the two heaviest inline modals —
AddApiKeyModal(~787-LOC body) andEditConnectionModal(~1091-LOC body) — plus sharedWebSessionCredentialGuide(~103 LOC) into standalone files underproviders/[id]/components/modals/andproviders/[id]/components/respectively. AddedERROR_TYPE_LABELSandformatTimeAgotoproviderPageHelpers.ts(leaf) soEditConnectionModalandConnectionRowshare them without cycles. Pruned 14 now-unused imports from the god-component.ProviderDetailPageClient.tsx: 9,981 → 8,092 LOC (−1,889 lines). Frozen baselines:AddApiKeyModal.tsx: 842,EditConnectionModal.tsx: 1170. 6 new Phase-1c smoke tests; all 21 vitest modal tests pass; typecheck/cycles/lint green. -
refactor: small db/utils cleanup ([#3523] — thanks @androw): table-driven
compression_analyticscolumn migration (replaces 17 repeatedALTER TABLEcalls), a single mergedserializeJsonFieldhelper indb/providers.ts(folded two byte-identical serializers), and removal of the dead no-opsyncProviderDataToCloud/getProvidersNeedingRefreshstubs fromshared/utils/machine.ts(no remaining callers). Pure refactor; behavior unchanged. -
Provider-detail god-component decomposition — Phase 2b (remaining shared helpers→leaf) ([#3501]): extended
providers/[id]/providerPageHelpers.tswith all remaining pure helpers needed by the heavy modals (AddApiKeyModal/EditConnectionModal) before they can be extracted. Moved 22 symbols: web-session credential label/hint/check/title helpers; upstream-headers helpers (upstreamHeadersRecordsEqual,headerRowsToRecord,effectiveUpstreamHeadersForProtocol,anyUpstreamHeadersBadge,getProtoSlice) plus theirHeaderDraftRow/CompatModelRow/CompatModelMap/CompatByProtocolMaptypes; Codex consts and helpers (CODEX_REASONING_STRENGTH_OPTIONS,CODEX_ACCOUNT_SERVICE_TIER_VALUES,CODEX_GLOBAL_SERVICE_MODE_VALUES,getCodexServiceTierLabel,normalizeCodexLimitPolicy,getCodexRequestDefaults,getClaudeCodeCompatibleRequestDefaults); misc helpers (compatProtocolLabelKey,extractCommandCodeCredentialInput,normalizeAndValidateHttpBaseUrl,SILICONFLOW_ENDPOINTS,CommandCodeAuthFlowState). New transitive imports wired into the leaf:MODEL_COMPAT_PROTOCOL_KEYS(@/shared/constants/modelCompat),CodexServiceTier/getCodexRequestDefaults/getClaudeCodeCompatibleRequestDefaults(@/lib/providers/requestDefaults),CodexGlobalServiceMode(@/lib/providers/codexFastTier),WebSessionCredentialRequirement(./webSessionCredentials).ProviderDetailPageClient.tsx: 10,288 → 9,980 LOC. Leaf module: 589 LOC (acyclic). 25-assertion unit test suite passes; smoke test 3/3; no import cycles. Co-authored with @oyi77. -
Provider-detail god-component decomposition — Phase 2 (helpers→lib) ([#3501]): extracted the pure shared helpers —
ProviderMessageTranslator/LocalProviderMetadatatypes,providerText/providerCountText/readBooleanToggle, and the provider base-URL + routing-tag/excluded-model parse/format block — into a new leafproviders/[id]/providerPageHelpers.ts(imports only@/shared, so the client and modals share them with no import cycle).ProviderDetailPageClient.tsx: 10,435 → 10,288 LOC. Unblocks extracting the heavierAddApiKeyModal/EditConnectionModal(which depend on these helpers) without cycling. The Phase 0 smoke test caught a missing transitive import (isSelfHostedChatProvider) at mount — now wired + locked by a new helpers unit test (12 assertions). Co-authored with @oyi77. -
#3500 fully resolved — Hard Rule #5 (no raw SQL in route handlers): all 13 internal offenders migrated to
src/lib/db/modules across slices (calllogs, usage_history/daily_usage_summary, community_servers, usage_logs, semantic_cache, proxy_logs, skills UPDATE, db-backups). The gate'sKNOWN_RAW_SQLset is renamed toEXTERNAL_DB_ALLOWED(with a back-compat alias) and now holds only the 2 external-DB reads (oauth/cursor/auto-import,oauth/kiro/auto-import) — these open _another app's SQLite to import credentials, so by design they cannot live in OmniRoute'sdb/domain. The gate still blocks any NEW raw SQL against OmniRoute's DB. -
chore(db-gate): reclassify
KNOWN_UNEXPORTED→INTENTIONALLY_INTERNALinscripts/check/check-db-rules.mjs([#3499]): a full audit of all 25 db modules confirmed each is consumed via direct/dynamic import per Hard Rule #2 ("Never barrel-import from localDb.ts"). The old framing labelled them as "debt", which was misleading — they are the correct pattern. The gate's blocking behaviour is unchanged (a NEW unexported module still fails); only the name, comments, and per-module justifications were updated to reflect audited truth. Four modules flaggedDEAD?(compressionScheduler,discovery,pluginMetrics,prompts) have zero production importers and are documented as schema-reserved. A new regression-guard test (tests/unit/check-db-rules-classification.test.ts) asserts every non-dead module in the set has ≥1 real importer, so a future consumer removal surfaces as a test failure requiring explicit reclassification. -
refactor(db): move
call_logsaggregations intocallLogStatsdb module ([#3500]): extracted raw SQL from three route handlers (/api/provider-metrics,/api/search/stats,/api/v1/search/analytics) into a newsrc/lib/db/callLogStats.tsdomain module (getProviderMetrics,getSearchProviderStats,getRecentSearchLogs,getSearchAggregateStats,getSearchProviderCounts). First slice of #3500 (call_logs cluster). Behavior unchanged; the three routes are removed fromKNOWN_RAW_SQLin the gate. Validated with TDD unit tests (6 assertions seeding an in-memory SQLite fixture). -
refactor(db): move
usage_history/daily_usage_summarySQL intousageAnalyticsdb module ([#3500]): extracted all inlinedb.prepare(...)calls from two route handlers (/api/usage/analytics,/api/settings/export-json) into a newsrc/lib/db/usageAnalytics.tsmodule and extendedsrc/lib/db/callLogStats.tswithgetFallbackStats. New exports:buildUnifiedSource,buildPresetUnifiedSource(UNION CTE builders), plus 12 typed query functions covering summary, daily, daily-cost, heatmap, model, provider, account, api-key, service-tier, weekly-pattern, and preset-cost aggregations, plusgetAllUsageHistory/getAllDomainCostHistory/getAllDomainBudgetsfor backup export. Second slice of #3500.KNOWN_RAW_SQLdrops from 12 → 10. Validated with 21 TDD unit tests (tests/unit/db-usage-analytics-3500.test.ts) seeding a temp SQLite fixture. -
refactor(db): move
community_serversauth look-up intogamificationdb module ([#3500]): extracted raw SQL from two federation route handlers (/api/gamification/federation/leaderboard,/api/gamification/federation/score) into a newgetConnectedServerByKeyHash(apiKeyHash)function insrc/lib/db/gamification.ts. Third slice of #3500 (gamification federation cluster). Behavior unchanged; the two routes are removed fromKNOWN_RAW_SQLin the gate. Validated with TDD unit tests (3 assertions seeding a temp SQLite fixture). -
refactor(db): move
skills UPDATE+db-backupsSQL into db modules ([#3500]): fifth slice of #3500. Extracted the dynamicUPDATE skills SET …fromsrc/app/api/skills/[id]/route.tsinto a newsrc/lib/db/skills.tsmodule (updateSkill(id, patch)). The dynamic SET clause is injection-safe: column names are validated against a hard-coded allowlist of known writable columns before being interpolated; unknown keys are silently ignored. Extendedsrc/lib/db/backup.tswith three new functions:exportAllSummaryRows()(multi-table SELECT for key_value / combos / provider_connections / api_keys, used by exportAll),getTableNamesFromAdapter()(sqlite_master introspection via an adapter arg, used by import validation), andcountImportedRows()(post-import COUNT(*) per table). The backup domain module is the correct home for sqlite_master introspection — it is not "raw SQL in a route" once moved there.KNOWN_RAW_SQLdrops by 3 (from 8 → 5). Validated with 11 TDD unit tests (tests/unit/db-backups-skills-3500.test.ts). -
refactor(db): move
usage_logs/semantic_cache/proxy_logsSQL into db modules ([#3500]): extracted rawdb.prepare(...)SQL from three route handlers (/api/analytics/auto-routing→usageLogs.ts;/api/cache/entries→semanticCache.ts;/api/logs/export→proxyLogs.ts) into newsrc/lib/db/domain modules. New exports:getAutoRoutingTotalCount,getAutoRoutingVariantBreakdown,getAutoRoutingTopProviders(usage_logs),listSemanticCacheEntries,deleteSemanticCacheBySignature,deleteSemanticCacheByModel(semantic_cache), andexportProxyLogsSince(proxy_logs). Fourth slice of #3500.KNOWN_RAW_SQLdrops from 8 → 5. Validated with 13 TDD unit tests (tests/unit/db-logs-cache-3500.test.ts) seeding temp SQLite fixtures. -
Provider-detail god-component decomposition — Phase 0 ([#3501]): introduced
ProviderDetailPageClient.tsxand reducedproviders/[id]/page.tsxto a thin 9-line route wrapper (was 12,882 LOC), following the repo's*PageClientconvention. Added the first-ever smoke render test for the page (Hard Rule #8) as the safety net every later extraction phase is diffed against. Behavior unchanged; thecheck-file-sizeratchet now tracks the extracted client. Foundation for Phases 1–6 (strangler-fig). Thanks @oyi77 for the parallel modularization effort in #3627. -
Provider-detail god-component decomposition — Phase 1a ([#3501]): extracted the three self-contained auth-import modal clusters (Codex/Claude/Gemini
Import*AuthModal+Apply*AuthModal+ their co-located helpers, ~2,160 LOC) intoproviders/[id]/components/modals/.ProviderDetailPageClient.tsxdrops 12,882 → 10,719 LOC. Behavior unchanged (smoke test green; clusters had clean{ onClose, onSuccess }/ inline-prop interfaces). Co-authored with @oyi77. -
Provider-detail god-component decomposition — Phase 1b ([#3501]): extracted
EditCompatibleNodeModal(+ its node/props types) intoproviders/[id]/components/modals/, and moved the sharedCC_COMPATIBLE_DEFAULT_CHAT_PATHconstant into a leafproviderDetailConstants.tsso the page client and the modal can both import it without a circular dependency. Also removed two dangling section comments left by Phase 1a.ProviderDetailPageClient.tsx: 10,719 → 10,435 LOC. Behavior unchanged (smoke test + a new standalone modal render test green;check:cyclesclean). Co-authored with @oyi77.
🔧 Bug Fixes
-
Combos / Auto-Combo: premature context compaction ("agent keeps forgetting things") ([#3680]): two related context-window bugs fixed. (1)
GET /api/combos/autonow advertisescontext_length/max_output_tokens(MAX across the candidate pool — safe because the auto-combo context pre-filter routes oversized requests to large-window candidates), and the opencode plugin consumes them instead of hardcodinglimit: { context: 0 }— a zero context silently disables opencode's smart auto-compaction, letting sessions grow until the gateway's destructive history purge kicks in. (2) chatCore's proactive compression for DB combos (incl. quota-shared pools) no longer compresses atmin(...allTargets): it now uses the EXECUTING target's own window (resolveComboContextLimit), keeping min-of-targets only as a defensive fallback when the current provider/model resolves no specific limit. TDD: 8 server tests (tests/unit/auto-combo-context-advertising.test.ts) + 3 plugin tests (tests/auto-combo-context.test.ts). -
Obsidian/WebDAV: add the
/api/settings/obsidian/webdavconfig route (enable/disable vault sync), encrypt WebDAV credentials at rest, and remove the duplicate UI block (#3485, part 1). -
OpenCode Free / passthrough: "Test all models" now respects "Auto-hide failed models" and switches the list to the visible filter so hidden models actually disappear (#3610). Three related bugs fixed:
autoHideFailedis now threaded from the outer component intoPassthroughModelsSectionvia a prop (single shared checkbox); the/api/models/test-allrequest body now includesautoHideFailed: trueso the server persists the hide; and after the loop,visibilityFilteris switched to"visible"when ≥1 model was hidden. Two pure-function helpers (buildPassthroughTestBody,shouldSwitchToVisibleFilter) extracted toproviderPageHelpers.tswith 7 unit tests. -
Resilience: clear stale transient connection cooldowns on startup so a prior unclean crash no longer makes every request time out at 120s after restart (#3625)
-
fix(home topology): restore live in-flight request pulse ([#3507]): the animated "pulse" edges in the home Provider Topology panel went dead after PR #3401 unified request visibility, because
activeRequestswas hardcoded to[]. Re-wired touseLiveRequests()(the existing WebSocket hook on port 20129) so that every pending/running request drives the animation in real time. A pureselectActiveRequestsmapping helper was extracted tohome/topologyUtils.tswith 5 unit tests. -
Electron desktop: launch the peer-stamping
server-ws.mjsentrypoint so local-only routes (AgentBridge, MCP, services) no longer return 403 LOCAL_ONLY (#3386) -
Provider Topology: stop flagging healthy providers as errored based on stale historical failures; use current request status (#3619)
-
OpenCode Free: fetch the live model catalog from the provider's
modelsUrlfor the no-auth model picker instead of serving a stale hardcoded list (#3611) -
Hermes Agent: honour the
HERMES_HOMEenv var when writing/reading the agent config instead of always using~/.hermes(#3628). IntroducedgetHermesHome()/getHermesConfigPath()helpers (read at call-time) and routed all four hardcoded callsites through them so OmniRoute's config lands in the same directory that the Hermes PowerShell installer configures on Windows. -
MITM/cert: remove the duplicated "Command failed:" prefix in system-command error messages (#3641):
execFileTextwas prepending its own"Command failed: "prefix on top of Node'sexecFileerror message, which already begins with"Command failed: <cmd>"for non-zero exits. The error message now surfaces Node's message directly (no double prefix), with stderr appended only when non-empty. -
fix(reasoning): replay
reasoning_contenton plain DeepSeek turns ([#3632] — thanks @adivekar-utexas): the reasoning-replay gate previously only fired when an assistant message already carriedreasoning_content. Plain (non-tool-call) turns whosereasoning_contentwas stripped by the client (e.g. Cursor) were forwarded without it, so DeepSeek V4+ rejected the request with 400 "the reasoning_content in the thinking mode must be passed back". The gate now also covers missing/emptyreasoning_contenton DeepSeek replay targets, injecting the cached reasoning (or the non-Anthropic placeholder) so multi-turn text conversations no longer 400. Fixes #1682. 2 regression tests. -
fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint ([#3631] — thanks @artickc): Kiro/CodeWhisperer access tokens and Q Developer profile ARNs are region-bound, so enterprise IAM Identity Center accounts outside
us-east-1(e.g.eu-central-1) were rejected by the default host. AddsresolveKiroRegion(stored region → profileArn region →us-east-1) andkiroRuntimeHost(regionalq.{region}.amazonaws.com, legacycodewhisperer.us-east-1for the default), routes chat + usage to the regional endpoint, and discovers the region-matchedprofileArnviaListAvailableProfilesin a best-effortpostExchangehook. 9 tests. -
fix(combo): skip same-provider/connection targets on connection-level errors ([#3637] — thanks @herjarsa): on connection-level upstream errors (408/500/502/503/504/524), remaining same-
provider:connectiontargets in a combo request are now skipped to avoid hammering a known-bad connection, in both the priority and round-robin paths. Adjusted in review to exclude OmniRoute circuit-breaker-open responses (503 +X-OmniRoute-Provider-Breaker/provider_circuit_open) from this skip, preserving the invariant that a breaker-open is an ordinary target failure (the next same-provider target is still tried). Co-authored with @herjarsa. -
/v1/responses: detect stream readiness for tool-call-only and
object-less chunks so Codex-shaped (reasoning + tools) requests no longer fail with "Stream ended before producing useful content" (#3612) -
RTL locales (ar/fa/he/ur): use logical CSS direction utilities for the sidebar and key overlays so the layout mirrors correctly under
dir=rtl(#3541, partial — core layout) -
Kiro/AWS auto-import: set a descriptive account name and dedupe by
profileArnso imports no longer create nameless duplicate "OAuth Account" rows (#3615) -
fix(guardrails): the
/api/guardrails/testroute now validates its body throughvalidateBody()(Zod) instead of parsing raw JSON directly, aligning it with the repo-wide input-validation pattern (Hard Rule #7). (#3621 — thanks @diegosouzapw) -
fix(dashboard): bulk provider connection actions — close audit, API, and UX gaps in the batch activate/deactivate flow: register the
provider.credentials.batch_updatedevent inHIGH_LEVEL_ACTIONSandACTIVITY_ICONS(was silently dropped from the Activity feed); fix/api/providersPATCH to returnwarnstatus whennotFoundis non-empty instead of alwayssuccess;/api/providers/test-batchempty-result early-return now includes asummaryso stale-ID mode reports to the user; bulk activate/deactivate chunks selection by 100 to avoid the Zod 400 cap on large provider accounts. (#3673 — thanks @leninejunior)
📝 Maintenance
- fix(ci): increase the
execFileSyncmaxBufferinvalidate-pack-artifactso the npm-pack inventory no longer overflows on large tarballs during release validation — follow-up to the v3.8.21 pack-artifact hotfix. (#3622 — thanks @diegosouzapw)
What's Changed
- fix: add reasoning token buffer for combo routing (fixes #3587) by @herjarsa in #3588
- Release v3.8.21 by @diegosouzapw in #3593
- fix(guardrails): use validateBody() in /api/guardrails/test route by @diegosouzapw in #3621
- fix(ci): increase execFileSync maxBuffer in validate-pack-artifact by @diegosouzapw in #3622
- fix(ci): align pack-artifact-policy with v3.8.21 package.json files expansion by @diegosouzapw in #3624
- Release v3.8.22 by @diegosouzapw in #3623
- fix(build): include webdav-handler.mjs in dist/ bundle by @diegosouzapw in #3687
Full Changelog: v3.8.20...v3.8.22