github diegosouzapw/OmniRoute v3.8.22

3 hours ago

✨ Added

  • MiMoCode free-tier provider ([#3659] — thanks @pizzav-xyz): new no-auth provider mimocode (alias mcode) exposing Xiaomi's mimo-auto model (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 generic NoAuthAccountCard dashboard component (also wired for opencode). 22 unit tests; upstream validated live during review. (Maintainer follow-up: added the required authHeader: "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 bare claude-* model IDs from Claude Code clients through the Claude Code OAuth account instead of requiring a provider prefix. Configurable via the OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS env 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/responses WebSocket calls are now persisted to request history — success completions plus prepare-failures, upstream WS errors and premature closes — with sanitizeErrorMessage applied to the stored error. Two proxy-side integration tests cover the success and failure paths.
  • Obsidian/WebDAV: add the /api/v1/webdav file 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 SQLite key_value table; credentials configured via PR1's /api/settings/obsidian/webdav endpoint. 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, useProviderModels hooks from the god-component — #3501 Phase 1f. ProviderDetailPageClient.tsx: 4,948 → 4,063 LOC (−885 lines). New hooks in hooks/: 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 useModelCompatState hook + 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 leaf hooks/useModelCompatState.ts (101 LOC); compat helpers moved to providerPageHelpers.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), and SiliconFlowEndpointModal from the god-component into components/#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) and EditConnectionModal (~1091-LOC body) — plus shared WebSessionCredentialGuide (~103 LOC) into standalone files under providers/[id]/components/modals/ and providers/[id]/components/ respectively. Added ERROR_TYPE_LABELS and formatTimeAgo to providerPageHelpers.ts (leaf) so EditConnectionModal and ConnectionRow share 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_analytics column migration (replaces 17 repeated ALTER TABLE calls), a single merged serializeJsonField helper in db/providers.ts (folded two byte-identical serializers), and removal of the dead no-op syncProviderDataToCloud/getProvidersNeedingRefresh stubs from shared/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.ts with 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 their HeaderDraftRow/CompatModelRow/CompatModelMap/CompatByProtocolMap types; 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/LocalProviderMetadata types, providerText/providerCountText/readBooleanToggle, and the provider base-URL + routing-tag/excluded-model parse/format block — into a new leaf providers/[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 heavier AddApiKeyModal/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's KNOWN_RAW_SQL set is renamed to EXTERNAL_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's db/ domain. The gate still blocks any NEW raw SQL against OmniRoute's DB.

  • chore(db-gate): reclassify KNOWN_UNEXPORTEDINTENTIONALLY_INTERNAL in scripts/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 flagged DEAD? (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_logs aggregations into callLogStats db module ([#3500]): extracted raw SQL from three route handlers (/api/provider-metrics, /api/search/stats, /api/v1/search/analytics) into a new src/lib/db/callLogStats.ts domain module (getProviderMetrics, getSearchProviderStats, getRecentSearchLogs, getSearchAggregateStats, getSearchProviderCounts). First slice of #3500 (call_logs cluster). Behavior unchanged; the three routes are removed from KNOWN_RAW_SQL in the gate. Validated with TDD unit tests (6 assertions seeding an in-memory SQLite fixture).

  • refactor(db): move usage_history/daily_usage_summary SQL into usageAnalytics db module ([#3500]): extracted all inline db.prepare(...) calls from two route handlers (/api/usage/analytics, /api/settings/export-json) into a new src/lib/db/usageAnalytics.ts module and extended src/lib/db/callLogStats.ts with getFallbackStats. 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, plus getAllUsageHistory/getAllDomainCostHistory/getAllDomainBudgets for backup export. Second slice of #3500. KNOWN_RAW_SQL drops 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_servers auth look-up into gamification db module ([#3500]): extracted raw SQL from two federation route handlers (/api/gamification/federation/leaderboard, /api/gamification/federation/score) into a new getConnectedServerByKeyHash(apiKeyHash) function in src/lib/db/gamification.ts. Third slice of #3500 (gamification federation cluster). Behavior unchanged; the two routes are removed from KNOWN_RAW_SQL in the gate. Validated with TDD unit tests (3 assertions seeding a temp SQLite fixture).

  • refactor(db): move skills UPDATE + db-backups SQL into db modules ([#3500]): fifth slice of #3500. Extracted the dynamic UPDATE skills SET … from src/app/api/skills/[id]/route.ts into a new src/lib/db/skills.ts module (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. Extended src/lib/db/backup.ts with 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), and countImportedRows() (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_SQL drops 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_logs SQL into db modules ([#3500]): extracted raw db.prepare(...) SQL from three route handlers (/api/analytics/auto-routingusageLogs.ts; /api/cache/entriessemanticCache.ts; /api/logs/exportproxyLogs.ts) into new src/lib/db/ domain modules. New exports: getAutoRoutingTotalCount, getAutoRoutingVariantBreakdown, getAutoRoutingTopProviders (usage_logs), listSemanticCacheEntries, deleteSemanticCacheBySignature, deleteSemanticCacheByModel (semantic_cache), and exportProxyLogsSince (proxy_logs). Fourth slice of #3500. KNOWN_RAW_SQL drops 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.tsx and reduced providers/[id]/page.tsx to a thin 9-line route wrapper (was 12,882 LOC), following the repo's *PageClient convention. 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; the check-file-size ratchet 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) into providers/[id]/components/modals/. ProviderDetailPageClient.tsx drops 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) into providers/[id]/components/modals/, and moved the shared CC_COMPATIBLE_DEFAULT_CHAT_PATH constant into a leaf providerDetailConstants.ts so 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:cycles clean). 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/auto now advertises context_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 hardcoding limit: { 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 at min(...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/webdav config 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: autoHideFailed is now threaded from the outer component into PassthroughModelsSection via a prop (single shared checkbox); the /api/models/test-all request body now includes autoHideFailed: true so the server persists the hide; and after the loop, visibilityFilter is switched to "visible" when ≥1 model was hidden. Two pure-function helpers (buildPassthroughTestBody, shouldSwitchToVisibleFilter) extracted to providerPageHelpers.ts with 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 activeRequests was hardcoded to []. Re-wired to useLiveRequests() (the existing WebSocket hook on port 20129) so that every pending/running request drives the animation in real time. A pure selectActiveRequests mapping helper was extracted to home/topologyUtils.ts with 5 unit tests.

  • Electron desktop: launch the peer-stamping server-ws.mjs entrypoint 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 modelsUrl for the no-auth model picker instead of serving a stale hardcoded list (#3611)

  • Hermes Agent: honour the HERMES_HOME env var when writing/reading the agent config instead of always using ~/.hermes (#3628). Introduced getHermesHome() / 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): execFileText was prepending its own "Command failed: " prefix on top of Node's execFile error 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_content on plain DeepSeek turns ([#3632] — thanks @adivekar-utexas): the reasoning-replay gate previously only fired when an assistant message already carried reasoning_content. Plain (non-tool-call) turns whose reasoning_content was 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/empty reasoning_content on 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. Adds resolveKiroRegion (stored region → profileArn region → us-east-1) and kiroRuntimeHost (regional q.{region}.amazonaws.com, legacy codewhisperer.us-east-1 for the default), routes chat + usage to the regional endpoint, and discovers the region-matched profileArn via ListAvailableProfiles in a best-effort postExchange hook. 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:connection targets 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 profileArn so imports no longer create nameless duplicate "OAuth Account" rows (#3615)

  • fix(guardrails): the /api/guardrails/test route now validates its body through validateBody() (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_updated event in HIGH_LEVEL_ACTIONS and ACTIVITY_ICONS (was silently dropped from the Activity feed); fix /api/providers PATCH to return warn status when notFound is non-empty instead of always success; /api/providers/test-batch empty-result early-return now includes a summary so 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 execFileSync maxBuffer in validate-pack-artifact so 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

Full Changelog: v3.8.20...v3.8.22

Don't miss a new OmniRoute release

NewReleases is sending notifications on new releases.